#!/usr/bin/env python3 """FairDinkumPublishing Gradio app — Ebook pipeline runner.""" from __future__ import annotations import json import os import shutil import subprocess import sys import time from pathlib import Path import gradio as gr # ── paths ────────────────────────────────────────────────────────────────── SPACE_ROOT = Path(__file__).resolve().parents[0] OUTPUT_DIR = SPACE_ROOT / "output" VAULT_DIR = SPACE_ROOT / "vault" OUTPUT_DIR.mkdir(exist_ok=True) VAULT_DIR.mkdir(exist_ok=True) PIPELINE_SCRIPT = SPACE_ROOT / "src" / "pipeline_runner.py" PYTHON = sys.executable def run_pipeline(niche: str) -> tuple[str, str | None, str | None]: """Run the ebook pipeline for the given niche. Returns (status, epub_path, pdf_path).""" if not niche or not niche.strip(): return "Please enter a niche or book topic.", None, None # Use timestamped output dir to avoid collisions ts = time.strftime("%Y%m%d_%H%M%S") out_root = OUTPUT_DIR / ts out_root.mkdir(parents=True, exist_ok=True) env = os.environ.copy() env["OBSIDIAN_VAULT"] = str(VAULT_DIR / ts) env["PIPELINE_OUT_ROOT"] = str(out_root) proc = subprocess.run( [PYTHON, str(PIPELINE_SCRIPT), niche.strip()], capture_output=True, text=True, timeout=600, env=env, ) stdout = proc.stdout.strip() stderr = proc.stderr.strip() if proc.returncode != 0: return f"Pipeline failed (exit {proc.returncode})\n{stderr or stdout}", None, None # Locate generated EPUB and PDF epub_files = sorted(out_root.glob("*.epub")) pdf_files = sorted(out_root.glob("*.pdf")) epub_path = str(epub_files[-1]) if epub_files else None pdf_path = str(pdf_files[-1]) if pdf_files else None status = f"{stdout}\n" if epub_path: size_mb = Path(epub_path).stat().st_size / 1024 / 1024 status += f"\nEPUB: {Path(epub_path).name} ({size_mb:.1f} MB)" if pdf_path: size_mb = Path(pdf_path).stat().st_size / 1024 / 1024 status += f"\nPDF: {Path(pdf_path).name} ({size_mb:.1f} MB)" return status, epub_path, pdf_path def get_vault_files() -> list[str]: """List vault directory slugs.""" if not VAULT_DIR.exists(): return [] return sorted([d.name for d in VAULT_DIR.iterdir() if d.is_dir()]) def load_concept(vault_slug: str) -> str: concept_path = VAULT_DIR / vault_slug / "meta" / "concept.json" if not concept_path.exists(): return "No concept found for this vault." return concept_path.read_text(encoding="utf-8") def build_ui() -> gr.Blocks: with gr.Blocks(title="Fair Dinkum Publishing", theme=gr.themes.Soft()) as demo: gr.Markdown( "# Fair Dinkum Publishing\n" "**SEO Geo-Keyword Ebook Pipeline** — Australian market, end-to-end.\n\n" "Enter a niche or location-based topic and the full pipeline will generate:\n" "concept → outline → chapters → covers → EPUB + PDF → Obsidian vault." ) with gr.Row(): niche_input = gr.Textbox( label="Book Niche / Topic", placeholder="e.g. Port Noarlunga jetty fishing, Christies Beach surfing...", lines=1, ) generate_btn = gr.Button("Generate Book", variant="primary") status_output = gr.Markdown(label="Pipeline Status") with gr.Row(): epub_dl = gr.File(label="EPUB Download") pdf_dl = gr.File(label="PDF Download") gr.Markdown("## Vault Browser") with gr.Row(): vault_dd = gr.Dropdown(label="Select Vault", choices=get_vault_files(), interactive=True) refresh_vault_btn = gr.Button("Refresh Vaults") concept_json = gr.Code(label="Concept JSON", language="json") vault_dd.change(fn=load_concept, inputs=vault_dd, outputs=concept_json) refresh_vault_btn.click(fn=lambda: gr.Dropdown(choices=get_vault_files()), outputs=vault_dd) def on_generate(niche): status, epub_path, pdf_path = run_pipeline(niche) # Refresh vault dropdown new_choices = get_vault_files() return ( status, epub_path, pdf_path, gr.Dropdown(choices=new_choices), ) generate_btn.click( fn=on_generate, inputs=niche_input, outputs=[status_output, epub_dl, pdf_dl, vault_dd], ) return demo demo = build_ui() if __name__ == "__main__": demo.launch()