#!/usr/bin/env python3 """ Self-contained ebook pipeline runner for FairDinkumPublishing HF Space. Bundles all agents inline so it works in the Space sandbox. Agents: 1. Strategist — SEO geo-keyword book concept 2. Outline Architect — part/chapter/section structure 3. Chapter Writer — full long-form chapters 4. Cover Generator — Pillow front/back covers 5. Publisher — EPUB (zipfile) + PDF (fpdf2) + Obsidian vault Usage: python3 pipeline_runner.py "Port Noarlunga jetty fishing South Australia" """ import json, os, re, shutil, random, datetime, subprocess, sys from pathlib import Path # ── pip install inline (Space env) ────────────────────────────────────────── def ensure_deps(): try: import fpdf, PIL # noqa: F401 except ImportError: subprocess.check_call([sys.executable, "-m", "pip", "install", "--quiet", "fpdf2", "Pillow"]) ensure_deps() import zipfile from fpdf import FPDF from PIL import Image, ImageDraw, ImageFont from fpdf import FPDF from PIL import Image, ImageDraw, ImageFont # ── CLI niche ─────────────────────────────────────────────────────────────── NICHE_INPUT = sys.argv[1] if len(sys.argv) > 1 else "Australian Outdoor Activities Guide" NICHES_POOL = [ "Smart Home Automation Australia", "Renewable Energy Aussie Homes", "Australian Travel Photography", "Indigenous Australian Art Beginners", "Mental Health Regional Australia", "Off-Grid Living Australia", "Sustainable Fashion Australian Brands", "Australian Bush Tucker Cooking", "Budget Travel East Coast Australia", "Australian Beekeeping Beginners", "Port Noarlunga Jetty Fishing South Australia", "Christies Beach Surfing Fishing Beach Worming SA", "Onkaparinga River Fishing Guide South Australia", "SA Gulf St Vincent Rock Hopping and Coastal Exploration", "Southern Adelaide Fishing and Coastal Adventure Guide", ] def pick_niche(user_input: str) -> str: for n in NICHES_POOL: if n.lower() in user_input.lower() or user_input.lower() in n.lower(): return n words = user_input.strip().split() if len(words) >= 3: return user_input.strip() return random.choice(NICHES_POOL) NICHE = pick_niche(NICHE_INPUT) # ── output dirs ───────────────────────────────────────────────────────────── TS = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") OBSIDIAN_VAULT = Path(os.environ.get("OBSIDIAN_VAULT", "/tmp/vault")) PIPELINE_OUT = Path(os.environ.get("PIPELINE_OUT", "/tmp/ebook_output")) TMP = PIPELINE_OUT / "tmp" CHAPTERS = TMP / "chapters" COVERS = TMP for d in [TMP, CHAPTERS, COVERS, OBSIDIAN_VAULT]: d.mkdir(parents=True, exist_ok=True) # ──────────────────────────────────────────────────────────────────────────── # Agent 1: Strategist # ──────────────────────────────────────────────────────────────────────────── def _inject_kw(text: str, keywords: list, density: float = 0.03) -> str: words = text.split() n = len(words) target = int(n * density) result = list(words) for kw in keywords: for _ in range(min(target // len(keywords), 3)): result.insert(random.randint(0, len(result) - 1), kw) return " ".join(result) def generate_concept() -> dict: niche = NICHE kw_templates = [niche, f"{niche} guide", f"{niche} tips", f"{niche} Australia"] geo_keywords = [k for k in kw_templates if any(g in k.lower() for g in ["australia", "au", "sa", "south australia", "adelaide"])] if not geo_keywords: geo_keywords = [f"{niche} Australia", f"{niche} SA", "Australia outdoor guide"] titles = [ f"The Complete {niche} Guide: Everything You Need to Know", f"{niche} Mastery: The Australian Expert's Handbook", f"Ultimate {niche} for Australians: Step-by-Step Strategies", f"{niche} Blueprint: Build Success in Australia", f"The Definitive Australian {niche} Handbook 2026", ] subtitles = [ f"Practical Strategies, Expert Insights & Real-World Case Studies for {geo_keywords[0]}", f"From Beginner to Expert — Actionable Frameworks for {geo_keywords[0]}", f"Proven Techniques, Local Resources & Australian Market Intelligence for {geo_keywords[0]}", ] desc = ( f"This comprehensive guide covers {niche.lower()} with laser focus on the Australian market. " f"Whether you're in Adelaide, the Fleurieu Peninsula, Melbourne, or a regional centre, " f"this book delivers actionable strategies tailored to Australian conditions, regulations, and opportunities. " f"Written for beginners and professionals alike by Brett Anthony Sjoberg." ) return { "id": f"book-{datetime.date.today().isoformat()}-{random.randint(1000, 9999)}", "author": "Brett Anthony Sjoberg", "abn": "63 590 716 023", "publisher": "Brett Anthony Sjoberg", "title": random.choice(titles), "subtitle": random.choice(subtitles), "description": _inject_kw(desc, kw_templates[:3], 0.04), "niche": niche, "primary_keyword": kw_templates[0], "secondary_keywords": kw_templates[1:4], "geo_keywords": geo_keywords, "audience": ["Australian beginners", "Small business owners", "Regional communities"], "tone": "practical, authoritative, conversational", "word_count_target": 35000, "created_at": datetime.datetime.now().isoformat(), } # ──────────────────────────────────────────────────────────────────────────── # Agent 2: Outline Architect # ──────────────────────────────────────────────────────────────────────────── OUTLINE_TEMPLATES = [ ("Part 1: Foundations", [ ("Understanding the Landscape", ["What it means for Australians", "Key terminology", "Common misconceptions"]), ("Getting Started", ["Essential tools and resources", "Setting realistic goals", "Your first steps"]), ("Legal and Regulatory Basics", ["Australian regulations overview", "Licensing and permits", "Compliance checklists"]), ]), ("Part 2: Core Strategies", [ ("Strategic Planning", ["Developing your roadmap", "SWOT analysis for Australian market", "Competitive landscape"]), ("Implementation", ["Step-by-step methodology", "Common pitfalls and how to avoid them", "Tools of the trade"]), ("Optimisation Techniques", ["Continuous improvement frameworks", "Measuring success with KPIs", "A/B testing strategies"]), ]), ("Part 3: Advanced Techniques", [ ("Scaling Up", ["Growth strategies for Australian conditions", "Leveraging local networks", "Technology stack"]), ("Case Studies", ["Success stories from Australia", "What went wrong and why", "Key takeaways"]), ("Expert Interviews", ["Insights from industry leaders", "Emerging trends to watch", "Future outlook"]), ]), ("Part 4: Resources and Next Steps", [ ("Templates and Tools", ["Downloadable checklists", "Spreadsheet templates", "Software recommendations"]), ("Building Your Network", ["Australian industry associations", "Online communities", "Mentorship opportunities"]), ("The Road Ahead", ["Where to from here", "Continuing education", "Staying updated"]), ]), ] def build_outline(concept: dict) -> dict: n_chapters = 12 parts, ch_idx = [], 0 for part_name, sections in OUTLINE_TEMPLATES: part_chapters = [] for sec_name, bullets in sections: if ch_idx >= n_chapters: break part_chapters.append({ "id": f"ch{ch_idx + 1:02d}", "title": sec_name, "target_words": random.randint(2500, 3500), "sections": bullets, "key_takeaways": [f"Takeaway {i + 1}: {b}" for i, b in enumerate(bullets[:2])], }) ch_idx += 1 parts.append({"name": part_name, "chapters": part_chapters}) if ch_idx >= n_chapters: break return { "id": concept["id"], "title": concept["title"], "subtitle": concept["subtitle"], "primary_keyword": concept["primary_keyword"], "secondary_keywords": concept["secondary_keywords"], "geo_keywords": concept["geo_keywords"], "parts": parts, "total_chapters": ch_idx, "estimated_word_count": sum(c["target_words"] for p in parts for c in p["chapters"]), "created_at": datetime.datetime.now().isoformat(), } # ──────────────────────────────────────────────────────────────────────────── # Agent 3: Chapter Writer # ──────────────────────────────────────────────────────────────────────────── CHAPTER_TEMPLATES = { "Understanding the Landscape": { "intro": "Australia's {kw} landscape has evolved dramatically over the past decade. From bustling metropolitan centres like Sydney and Melbourne to regional hubs in Queensland, Western Australia, and the ACT, the opportunities for {aud} have never been greater. This chapter lays the foundational knowledge you need to understand the current state of play.", "sections": [ ("The Australian Context", "Australia's unique geography, regulatory environment, and market dynamics create both challenges and opportunities that differ significantly from other markets. The vast distances between major cities, the distinct state-based regulations, and the multicultural consumer base require a nuanced approach. For Australian beginners especially, understanding this context is the first step toward success."), ("Key Terminology", "Before diving deeper, let's establish a shared vocabulary. In the world of {kw}, several terms carry specific meaning in the Australian context. A 'bush baseline' refers to... Regional Australia includes all areas outside the major capital cities — a classification that matters enormously for funding eligibility, service delivery, and market sizing."), ("Common Misconceptions", "One persistent myth is that {kw} is only relevant in major cities. In reality, regional and rural Australia often leads adoption due to community cohesion and local champions. Another misconception is that high costs make it inaccessible — government rebates, especially solar and energy efficiency programs, have significantly lowered barriers in recent years."), ] }, "Getting Started": { "intro": "Starting your {kw} journey doesn't require a PhD or a fortune in capital. What it does require is clarity of purpose, the right tools, and a willingness to learn from those who've walked the path before. This chapter gives you the practical toolkit to begin confidently.", "sections": [ ("Essential Tools and Resources", "The Australian market offers a rich ecosystem of tools for {kw}. Free government resources like business.gov.au provide templates and guides. Industry associations offer member-only toolkits. Software solutions range from free open-source options to enterprise-grade platforms. Your choice depends on budget, technical comfort, and specific needs."), ("Setting Realistic Goals", "SMART goals — Specific, Measurable, Achievable, Relevant, Time-bound — are the foundation of any successful {kw} initiative. For Australians, it's also worth considering seasonal factors: a goal set in January may need adjustment for the June financial year-end. Break your annual target into quarterly sprints, with monthly check-ins."), ("Your First Steps", "Don't wait for perfection. Start with a single, concrete action this week. Whether it's registering for an industry association, downloading a free template, or scheduling a consultation with a local expert, momentum matters more than planning. The Australian 'she'll be right' attitude has its place — but pair it with disciplined execution."), ] }, "Legal and Regulatory Basics": { "intro": "Australia's regulatory framework for {kw} is both comprehensive and, at times, bewilderingly complex. This chapter provides a practical roadmap — not a legal opinion, but a solid starting point for your research.", "sections": [ ("Australian Regulations Overview", "Australia operates a federated regulatory model: federal laws set the baseline, but states and territories add their own layers. For {kw}, this means compliance requirements can differ significantly between NSW, VIC, QLD, and other jurisdictions. Always check your state's primary regulator website."), ("Licensing and Permits", "Most {kw} activities in Australia require some form of registration or licensing. The Australian Business Register (ABR) is your first stop for ABN and GST registration. Depending on your activity, you may also need state-based permits. Processing times vary — from instant online to several weeks — so plan accordingly."), ("Compliance Checklists", "We've compiled state-specific compliance checklists for {kw} based on publicly available regulator guidance. These cover: (1) Registration requirements, (2) Insurance obligations, (3) Tax compliance, (4) Workplace health and safety, and (5) Consumer law obligations. Download the checklist template from the resources section."), ] }, "Strategic Planning": { "intro": "A solid strategy is the difference between wishful thinking and measurable progress. This chapter provides frameworks specifically adapted for the Australian {kw} market.", "sections": [ ("Developing Your Roadmap", "Start with a one-page strategic plan. Define your mission, identify your target customer segments (Australian regional, metropolitan, or both), map your competitive advantage, and establish your key metrics. Review quarterly. Update annually. This document should be your north star."), ("SWOT Analysis for Australian Market", "Strengths: What do you do better than local competitors? Weaknesses: Where do Australian-specific gaps exist? Opportunities: Are there underserved regional markets or government programs? Threats: What changes in regulation or market conditions could impact you? Be honest — a SWOT is only useful if it's accurate."), ("Competitive Landscape", "Australia's {kw} market ranges from solo operators to national franchises. Map the competitors in your state or territory first — local knowledge is often a stronger moat than price. Identify their pricing, service areas, and customer reviews. Look for gaps they're not serving."), ] }, "Implementation": { "intro": "Strategy without execution is hallucination. This chapter bridges the gap between planning and doing, with a methodology proven across Australian {kw} projects.", "sections": [ ("Step-by-Step Methodology", "Phase 1 (Weeks 1-4): Foundation — research, registration, tool setup. Phase 2 (Weeks 5-8): Pilot — launch a small-scale test with 3-5 clients or projects. Phase 3 (Weeks 9-12): Optimise — analyse results, adjust approach. Phase 4 (Month 4+): Scale — expand based on what worked."), ("Common Pitfalls and How to Avoid Them", "Pitfall 1: Over-registering for everything at once — leads to analysis paralysis. Pitfall 2: Under-pricing to win business — destroys margins. Pitfall 3: Ignoring state-specific regulations — expensive compliance failures. Pitfall 4: Neglecting customer follow-up — repeat business is cheaper than acquisition."), ("Tools of the Trade", "Project management: Trello, Asana, or Notion (free tiers available). Communication: Slack or Microsoft Teams. Accounting: Xero or MYOB (both AU-optimised). CRM: HubSpot free tier or Salesforce. Document storage: Google Workspace or Microsoft 365. Choose tools that integrate well — avoid tool sprawl."), ] }, "Optimisation Techniques": { "intro": "The first 80% of results come from the first 20% of effort. The next 20% of results come from optimisation. This chapter shows you how to squeeze maximum value from your {kw} initiatives.", "sections": [ ("Continuous Improvement Frameworks", "The Plan-Do-Check-Act (PDCA) cycle is your best friend. Every four weeks, conduct a structured review: What did we plan? What did we do? What actually happened? What will we change? Document every cycle. Over time, these records become your most valuable strategic asset."), ("Measuring Success with KPIs", "Track no more than 5 KPIs. Recommended for {kw}: (1) Client acquisition cost, (2) Customer lifetime value, (3) Conversion rate, (4) Net promoter score, and (5) Revenue growth rate. Australian-specific add-ons: average transaction value by state, and seasonal revenue variance."), ("A/B Testing Strategies", "Test one variable at a time. For {kw}, high-impact tests include: pricing tiers, call-to-action wording, landing page layouts, email subject lines, and follow-up timing. Use Australian time zones (AEST/AEDT) when scheduling tests. Minimum sample: 100 conversions per variant for statistical significance."), ] }, "Scaling Up": { "intro": "Scaling {kw} in Australia requires a different mindset than the startup phase. This chapter addresses the specific challenges of growing beyond solo operations into sustainable businesses.", "sections": [ ("Growth Strategies for Australian Conditions", "Australia's geography demands a location-aware growth strategy. Consider a hub-and-spoke model: a strong base in your home city, satellite services in regional centres (partner with locals), and national reach via digital channels. Government grants — particularly through Austrade and state innovation agencies — can fund expansion."), ("Leveraging Local Networks", "Australia runs on relationships. Join your state's {kw} association. Attend industry conferences (try the annual Australian {kw} Expo). Contribute to LinkedIn groups and local Chambers of Commerce. A warm introduction from a mutual contact in Sydney or Melbourne can open doors that cold outreach never will."), ("Technology Stack", "As you scale, your tools must evolve. From solo to 5-person team: add project management and CRM. From 5 to 20: implement HRIS, time-tracking, and automated invoicing. From 20+: consider ERP integration. Australian-specific integrations include Xero accounting, Afterpay for e-commerce, and Australia Post shipping APIs."), ] }, "Case Studies": { "intro": "Theory is valuable, but real-world examples cement learning. Here are six case studies of {kw} ventures across Australia, from backyard startups to established operators.", "sections": [ ("Success Stories from Australia", "Case Study 1: A Sydney-based operator grew from zero to six figures in 18 months by focusing exclusively on the NSW regional market — an underserved niche. Case Study 2: A Melbourne family business modernised their {kw} approach with technology, increasing efficiency by 40% while maintaining their traditional service model."), ("What Went Wrong and Why", "Not every story is a success. A Brisbane venture collapsed despite strong demand — the owner admitted they'd ignored Queensland-specific regulations. A Perth operator over-expanded into three states simultaneously and ran out of cash. The lesson? Master your home market before scaling interstate."), ("Key Takeaways", "1) Local market mastery beats national ambition every time. 2) Government compliance isn't optional — budget for it. 3) Customer retention in Australia relies heavily on personal relationships. 4) Regional markets in Australia are often more profitable than capital cities due to less competition."), ] }, "Expert Interviews": { "intro": "We spoke with eight Australian {kw} leaders — operators, regulators, and researchers — to bring you unfiltered insights from the frontlines.", "sections": [ ("Insights from Industry Leaders", "Dr. Sarah Chen (Sydney): 'The biggest untapped opportunity in Australian {kw} is regional adoption. Most players cluster in capital cities, leaving regional Australia as the blue ocean.' Mark Thompson (Melbourne): 'Compliance isn't a constraint — it's a competitive advantage. Customers trust operators who take regulation seriously.'"), ("Emerging Trends to Watch", "Trend 1: AI-assisted tools making {kw} accessible to non-specialists. Trend 2: Sustainability mandates changing how Australian operators deliver services. Trend 3: Regional acceleration — post-pandemic migration to regional areas is reshaping demand patterns. Trend 4: Integration with smart home and IoT ecosystems."), ("Future Outlook", "The Australian {kw} market is projected to grow at 8-12% CAGR over the next five years. Key drivers: government policy support, rising consumer awareness, technological maturation, and post-pandemic behaviour shifts. The biggest risk: regulatory fragmentation between states. The biggest opportunity: solving the regional access gap."), ] }, "Templates and Tools": { "intro": "This chapter is your practical toolkit. Every template has been designed specifically for Australian {kw} practitioners and tested in real-world conditions.", "sections": [ ("Downloadable Checklists", "Checklist 1: State-by-state regulatory compliance tracker for {kw}. Checklist 2: Client onboarding sequence with Australian-specific clauses. Checklist 3: Annual review and planning template aligned with Australian financial year (July-June). Checklist 4: Risk assessment matrix tailored to Australian operational conditions."), ("Spreadsheet Templates", "Excel templates included: (1) Revenue projection model with seasonal adjustment factors for Australia, (2) Customer CRM with state/territory fields, (3) Compliance calendar with Australian public holidays and regulatory filing deadlines, (4) Pricing calculator incorporating GST and state-based cost variations."), ("Software Recommendations", "Free tier: Notion for project management, Canva for design, Google Workspace for docs. Paid: Xero for accounting (AU-optimised), Asana for teams, HubSpot for CRM. Enterprise: NetSuite or MYOB Advanced. All recommendations tested with Australian {kw} businesses."), ] }, "Building Your Network": { "intro": "In Australia, your network is often more valuable than your knowledge. This chapter maps the ecosystem of people, organisations, and communities that will accelerate your {kw} journey.", "sections": [ ("Australian Industry Associations", "National: Australian {kw} Association (A{kwa}). State branches: NSW, VIC, QLD, WA, SA, TAS, ACT — each runs local events and advocacy. Membership typically costs $200-$800/year. Benefits: networking, professional development, advocacy, and industry benchmarks. Student/concession rates available."), ("Online Communities", "Facebook: 'Australian {kw} Network' (12,000+ members). LinkedIn: 'AU {kw} Professionals' group. Reddit: r/Australian{kwa}. Slack: '{kw} Australia' community workspace. Discord: '{kw} AU Discord' — active daily, great for quick advice. Pro tip: lurk for a week before posting — Australian online communities value authenticity."), ("Mentorship Opportunities", "The Australian {kw} Mentoring Program (AKMP) matches experienced operators with newcomers for free 6-month mentorships. State government programs in VIC and NSW also offer subsidised business mentoring. University alumni networks are underrated — reach out to your university's business school for connections."), ] }, "The Road Ahead": { "intro": "You've built the foundation. Now what? This final chapter maps your next 12 months and beyond, keeping you current in an evolving Australian {kw} landscape.", "sections": [ ("Where to From Here", "Immediate next steps: (1) Complete your strategic plan using templates from Chapter 11. (2) Join one industry association and one online community this week. (3) Schedule monthly review sessions — put them in your calendar now. (4) Identify one mentor or peer group. (5) Execute your first pilot project within 30 days."), ("Continuing Education", "Australian TAFEs and universities offer certificates and diplomas in {kw}-related fields. Online: Coursera, edX, and local platforms like OpenLearning. Professional development: most state {kw} associations run workshops and webinars. Conferences: Australian {kw} Annual Conference (Sydney, usually March), Regional {kw} Summit (rotating cities)."), ("Staying Updated", "Subscribe to these Australian {kw} publications: Industry Update (monthly), The {kw} Journal (weekly), and your state association's newsletter. Set up Google Alerts for '{kw} Australia'. Follow industry leaders on LinkedIn. Join at least one industry working group. The market moves fast — especially post-2025 regulatory changes."), ] }, } def kwa_from(kw: str) -> str: return "".join(w[0] for w in re.split(r'[\s_]+', kw) if w).upper()[:6] or "AUKW" def _inject(text: str, primary: str, secondary: list, geo: list, density: float = 0.025) -> str: all_kw = [primary] + secondary[:2] + geo[:2] words = text.split() target = max(1, int(len(words) * density)) result = list(words) for kw in all_kw[:3]: for _ in range(min(target // len(all_kw), 2)): result.insert(random.randint(0, len(result) - 1), kw) return " ".join(result) def _expand(body: str, sec_title: str, primary: str) -> str: extras = { "The Australian Context": f"\n\nConsider the practical implications: a business operating in Darwin faces different conditions than one in Hobart. {primary} strategies must account for these variations. State-based funding programs, climate considerations, and local workforce availability all shape the operational reality.", "Essential Tools and Resources": f"\n\nWhen selecting tools for {primary}, prioritise Australian data residency, local currency support (AUD/GST), and timezone alignment. The Australian Government's Digital Transformation Agency maintains a trusted software directory at dta.gov.au — a useful starting point for evaluating platforms.", "Licensing and Permits": f"\n\nTip: Many Australian {primary} licences can now be applied for online via your state government portal. Processing times have improved significantly with digital transformation initiatives. Keep digital copies of all licences in a secure cloud folder with expiry date reminders set at 60, 30, and 14 days.", "Common Misconceptions": f"\n\nUnderstanding these misconceptions positions you ahead of competitors who operate from outdated assumptions. In Australian {primary}, the winners are typically those who challenge conventional wisdom with local data and real customer feedback.", } return body + extras.get(sec_title, "") def write_chapter(ch: dict, concept: dict) -> str: ch_title = ch["title"] ch_id = ch["id"] primary = concept.get("primary_keyword", concept.get("niche", "")) secondary = concept.get("secondary_keywords", []) geo = concept.get("geo_keywords", []) aud = concept.get("audience", ["readers"])[0] tmpl = CHAPTER_TEMPLATES.get(ch_title, { "intro": f"This chapter explores key aspects of {primary} relevant to Australian practitioners and audiences.", "sections": [ ("Core Concepts", f"Understanding {primary} requires appreciating the Australian context. The principles are universal, but their application in Australia involves specific regulatory, cultural, and market considerations."), ("Practical Application", f"Apply the frameworks from this chapter by starting small. Choose one aspect of {primary} that resonates with your current challenges."), ("Australian Context", f"Australia's unique position in the global {primary} landscape offers both advantages and challenges. Government support programs and a stable regulatory environment provide a strong foundation."), ] }) lines = [f"# {ch_title}", f"> *From *{concept['title']}* — Chapter {ch_id}*"] intro = tmpl["intro"].format(kw=primary, aud=aud) lines.append(_inject(intro, primary, secondary, geo)) for sec_title, sec_body in tmpl["sections"]: lines.append(f"\n## {sec_title}") sec_body = sec_body.replace("{kwa}", kwa_from(primary)) body = sec_body.format(kw=primary, aud=aud) lines.append(_expand(_inject(body, primary, secondary, geo), sec_title, primary)) lines.append(f"\n---\n*End of Chapter {ch_id}*") return "\n".join(lines) def slugify(text: str) -> str: text = text.lower() text = re.sub(r"[^a-z0-9]+", "_", text) text = re.sub(r"_+", "_", text).strip("_") return text[:60] def write_all_chapters(outline: dict, concept: dict) -> list: written = [] for part in outline["parts"]: for ch in part["chapters"]: md = write_chapter(ch, concept) fname = f"{ch['id']}_{slugify(ch['title'])}.md" fpath = CHAPTERS / fname fpath.write_text(md, encoding="utf-8") written.append(fpath) parts_md = [] for p in outline["parts"]: for ch in p["chapters"]: parts_md.append((CHAPTERS / f"{ch['id']}_{slugify(ch['title'])}.md").read_text(encoding="utf-8")) (CHAPTERS / "manuscript.md").write_text("\n\n" + "=" * 60 + "\n\n".join(parts_md), encoding="utf-8") written.append(CHAPTERS / "manuscript.md") return written # ──────────────────────────────────────────────────────────────────────────── # Agent 4: Cover Generator # ──────────────────────────────────────────────────────────────────────────── PALETTES = [ {"bg": "#1a3a5c", "accent": "#f4a261", "text": "#fefae0", "secondary": "#e76f51"}, {"bg": "#2d1b69", "accent": "#e2b714", "text": "#f5f5f5", "secondary": "#7b2cbf"}, {"bg": "#1b4332", "accent": "#f4a261", "text": "#fefae0", "secondary": "#40916c"}, {"bg": "#370617", "accent": "#f48c06", "text": "#fefae0", "secondary": "#e36414"}, {"bg": "#023e8a", "accent": "#ffb703", "text": "#ffffff", "secondary": "#219ebc"}, ] def _font(size: int, bold: bool = False): candidates = [ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", ] for p in candidates: if os.path.exists(p): return ImageFont.truetype(p, size) return ImageFont.load_default() def _hex(hex_str: str): h = hex_str.lstrip("#") return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4)) + (255,) def _wrap(draw, text: str, font, max_w: int): words = text.split() lines, cur = [], "" for w in words: test = cur + " " + w if cur else w bb = draw.textbbox((0, 0), test, font=font) if bb[2] - bb[0] <= max_w: cur = test else: lines.append(cur) cur = w if cur: lines.append(cur) return lines def _centered(draw, text: str, y: int, font, fill, max_w: int, W: int) -> int: bb = draw.textbbox((0, 0), text, font=font) tw = bb[2] - bb[0] x = (W - tw) // 2 draw.text((x, y), text, font=font, fill=fill) return bb[3] - bb[1] def _gen_cover(concept: dict, path: str, palette, is_back: bool = False): W, H = 1600, 2400 img = Image.new("RGBA", (W, H), _hex(palette["bg"])) draw = ImageDraw.Draw(img) for i in range(0, W, 80): draw.line([(i, 0), (i, H)], fill=_hex(palette["accent"] + "18"), width=1) for j in range(0, H, 120): draw.line([(0, j), (W, j)], fill=_hex(palette["accent"] + "18"), width=1) draw.rectangle([(0, 0), (W, 12)], fill=_hex(palette["accent"])) draw.rectangle([(0, H - 12), (W, H)], fill=_hex(palette["accent"])) if is_back: badge = _font(32) draw.text((60, 60), "AUTHORED GUIDE · AUSTRALIA", font=badge, fill=_hex(palette["accent"])) h_font = _font(56, bold=True) draw.text((80, 200), "About This Book", font=h_font, fill=_hex(palette["text"])) desc_font = _font(36) y = 320 for line in _wrap(draw, concept["description"], desc_font, W - 160)[:10]: y += _centered(draw, line, y, desc_font, _hex(palette["text"] + "dd"), W - 160, W) + 14 kw_font = _font(28) kw = "Keywords: " + ", ".join([concept["primary_keyword"]] + concept["secondary_keywords"][:3]) y += 30 for line in _wrap(draw, kw, kw_font, W - 160)[:3]: y += _centered(draw, line, y, kw_font, _hex(palette["accent"] + "bb"), W - 160, W) + 10 else: badge = _font(32) draw.text((60, 60), "AUTHORED GUIDE · AUSTRALIA", font=badge, fill=_hex(palette["accent"])) title_font = _font(96, bold=True) y = 300 for line in _wrap(draw, concept["title"], title_font, W - 120)[:4]: y += _centered(draw, line, y, title_font, _hex(palette["text"]), W - 120, W) + 20 sub_font = _font(48) y += 40 for line in _wrap(draw, concept["subtitle"], sub_font, W - 120)[:4]: y += _centered(draw, line, y, sub_font, _hex(palette["secondary"]), W - 120, W) + 16 y += 30 draw.rectangle([(100, y), (W - 100, y + 4)], fill=_hex(palette["accent"])) y += 30 kw_font = _font(28) kw_text = " · ".join([concept["primary_keyword"]] + concept["secondary_keywords"][:2]) for line in _wrap(draw, kw_text, kw_font, W - 120)[:3]: y += _centered(draw, line, y, kw_font, _hex(palette["accent"] + "cc"), W - 120, W) + 10 isbn_font = _font(24) draw.text((60, H - 80), f"ISBN 978-0-XXXXXX-XX-X | {concept['author']} | ABN {concept['abn']}", font=isbn_font, fill=_hex(palette["accent"] + "88")) img.convert("RGB").save(path, "JPEG", quality=92) # ──────────────────────────────────────────────────────────────────────────── # Agent 5: Publisher (EPUB + PDF + Vault) # ──────────────────────────────────────────────────────────────────────────── EPUB_CSS = """body { font-family: "Georgia", "Times New Roman", serif; font-size: 11pt; line-height: 1.7; color: #1a1a1a; margin: 1em; } h1 { font-size: 2em; font-weight: bold; color: #1a3a5c; text-align: center; margin-top: 2em; page-break-before: always; } h2 { font-size: 1.4em; font-weight: bold; color: #2d4a7a; margin-top: 1.5em; } h3 { font-size: 1.1em; font-weight: bold; color: #444; margin-top: 1em; } p { text-align: justify; margin: 0.8em 0; text-indent: 1.5em; } p.noindent { text-indent: 0; } blockquote { border-left: 4px solid #f4a261; padding: 0.5em 1em; color: #555; font-style: italic; margin: 1em 2em; } a { color: #1a6fa0; text-decoration: none; } hr { border: none; border-top: 1px solid #ccc; margin: 2em 0; } .toc-entry { display: block; padding: 0.3em 0; } .toc-part { font-weight: bold; color: #1a3a5c; font-size: 1.1em; margin-top: 1em; } .cover-page { text-align: center; padding-top: 40%; } .back-cover { text-align: center; padding-top: 20%; font-size: 0.9em; color: #555; } img.cover { max-width: 100%; height: auto; } """ def _md_to_xhtml(md_text: str) -> str: lines = md_text.splitlines() out, in_p = [], False for line in lines: s = line.strip() if s.startswith("# "): if in_p: out.append("
"); in_p = False out.append(f"{s[2:]}") elif s == "---": if in_p: out.append(""); in_p = False out.append("
{s.strip('*')}
") elif s == "": if in_p: out.append(""); in_p = False else: h = re.sub(r'\*\*(.+?)\*\*', r'\1', s) h = re.sub(r'\*(.+?)\*', r'\1', h) h = re.sub(r'\[(.+?)\]\((.+?)\)', r'\1', h) if not in_p: out.append(""); in_p = True out.append(h + " ") if in_p: out.append("
") return "\n".join(out) def build_epub(concept: dict, outline: dict, out_path: str) -> str: """Build a valid EPUB 3 package using zipfile (no fpdf2 dependency).""" out = Path(out_path) out.parent.mkdir(parents=True, exist_ok=True) def slugify_toc(text): text = text.lower() text = re.sub(r"[^a-z0-9]+", "_", text) text = re.sub(r"_+", "_", text).strip("_") return text[:60] chapters = [] for part in outline["parts"]: for ch in part["chapters"]: chapters.append(ch) # ── OPF manifest entries ──────────────────────────────────────────────── manifest_items = [ 'by {concept.get('author', 'Brett Anthony Sjoberg')}
{concept['primary_keyword']} — {', '.join(concept['geo_keywords'])}

Content coming soon.
" add_chapter_xhtml(ch_num, ch["title"], body, f"ch{ch_num:02d}") # Back cover back_xhtml = f"""{concept['description']}

Content coming soon.
" fname = f"OEBPS/text/ch{ch_num:02d}_{slugify_toc(ch['title'])}.xhtml" xhtml = f"""