"""Gradio web application for DenseFeed pipeline — thin shell importing from web/.""" from __future__ import annotations import logging import gradio as gr from densefeed.config import load_config from densefeed.web.auth import ( space_default_config, set_initial_state, get_backend as _get_backend, ) from densefeed.web.prefs import ( load_user_preferences, save_user_preferences, bootstrap_repositories, bootstrap_logged_in_user, ) from densefeed.web.pipeline_service import ( start_generation, run_single_stage, fetch_with_sources, list_run_history, latest_audio_download_link, ) logger = logging.getLogger(__name__) STAGES = [ (1, "Fetch", "Collect news from RSS, HN, arXiv, GitHub"), (2, "Rank", "Two-pass LLM ranking"), (3, "Extract", "Full article extraction via Jina Reader"), (4, "Script", "Two-host podcast dialogue generation"), (5, "Synthesize", "edge-tts audio synthesis"), (6, "Publish", "RSS feed + storage + notification"), ] def create_ui() -> gr.Blocks: cfg = space_default_config(load_config()) backend = _get_backend() set_initial_state(cfg, backend) with gr.Blocks(title="DenseFeed") as demo: gr.Markdown("# DenseFeed\nDaily tech news podcast pipeline") run_id_state = gr.State("") with gr.Row(): gr.LoginButton() auth_status = gr.Textbox(label="Status", value="Logged out. Please log in with Hugging Face to generate.", interactive=False) with gr.Row(): date_input = gr.Textbox( label="Date (YYYY-MM-DD)", placeholder="Leave empty for today" ) with gr.Tabs(): # -- Generate Tab -- with gr.Tab("Generate"): run_btn = gr.Button("Generate Podcast", variant="primary", interactive=False) output = gr.Textbox(label="Output", lines=3) run_btn.click( fn=start_generation, inputs=date_input, outputs=[output, run_id_state], ) # -- History Tab -- with gr.Tab("History"): refresh_btn = gr.Button("Refresh") latest_audio_md = gr.Markdown("No podcast audio is available yet.") history_df = gr.Dataframe( headers=["Run ID", "Date", "Status", "Started", "Audio URL"], label="Recent Runs", interactive=False, ) def refresh_history( token: gr.OAuthToken | None = None, request: gr.Request | None = None, ): return list_run_history(token, request), latest_audio_download_link(token, request) refresh_btn.click( fn=refresh_history, outputs=[history_df, latest_audio_md], ) # -- Preferences Tab -- with gr.Tab("Preferences"): gr.Markdown("Customize your podcast preferences") load_btn = gr.Button("Load My Preferences") topics_input = gr.Textbox( label="Topics (comma-separated)", placeholder="e.g. rust, security, LLM", ) excluded_input = gr.Textbox( label="Excluded Sources (comma-separated)", placeholder="e.g. arxiv, github", ) rate_a_input = gr.Textbox(label="Speaker A Rate", value="+10%") rate_b_input = gr.Textbox(label="Speaker B Rate", value="+10%") cross_pause_input = gr.Number( label="Cross-speaker pause (ms)", value=550 ) same_pause_input = gr.Number(label="Same-speaker pause (ms)", value=250) min_rel_input = gr.Number(label="Min relevance score", value=6) top_n_input = gr.Number(label="Top N stories", value=12) gr.Markdown("### Hugging Face Storage Configuration") hf_dataset_repo_input = gr.Textbox( label="Hugging Face Dataset Repo (for DuckDB & run log storage)", placeholder="username/repo-name (e.g. yourname/densefeed-db)", ) hf_audio_repo_input = gr.Textbox( label="Hugging Face Audio Repo (for hosting generated MP3 files)", placeholder="username/repo-name (e.g. yourname/densefeed-audio)", ) bootstrap_btn = gr.Button( "Bootstrap Hugging Face Repositories", variant="secondary" ) bootstrap_status = gr.Textbox( label="Bootstrap Status", interactive=False ) pref_status = gr.Textbox(label="Status", interactive=False) pref_inputs = [ topics_input, excluded_input, rate_a_input, rate_b_input, cross_pause_input, same_pause_input, min_rel_input, top_n_input, hf_dataset_repo_input, hf_audio_repo_input, ] save_btn = gr.Button("Save Preferences", variant="primary") save_btn.click( fn=save_user_preferences, inputs=pref_inputs, outputs=pref_status, ) load_btn.click( fn=load_user_preferences, outputs=pref_inputs, ) bootstrap_btn.click( fn=bootstrap_repositories, inputs=[hf_dataset_repo_input, hf_audio_repo_input], outputs=bootstrap_status, ) # -- Individual Stages Tab (advanced) -- with gr.Tab("Individual Stages"): for stage_num, stage_name, stage_desc in STAGES: with gr.Accordion( f"Stage {stage_num}: {stage_name}", open=(stage_num == 1) ): gr.Markdown(stage_desc) if stage_num == 1: with gr.Row(): rss_chk = gr.Checkbox(value=True, label="RSS") hn_chk = gr.Checkbox(value=True, label="HN") arxiv_chk = gr.Checkbox(value=True, label="arXiv") gh_chk = gr.Checkbox(value=True, label="GitHub") s_btn = gr.Button(f"Run Stage {stage_num}") s_out = gr.Textbox(label="Result") s_btn.click( fn=fetch_with_sources, inputs=[date_input, rss_chk, hn_chk, arxiv_chk, gh_chk], outputs=s_out, ) else: def make_fn(n): def stage_fn( d, tok: gr.OAuthToken | None = None, request: gr.Request | None = None, ): return run_single_stage(d, n, tok, request) return stage_fn s_btn = gr.Button(f"Run Stage {stage_num}") s_out = gr.Textbox(label="Result") s_btn.click( fn=make_fn(stage_num), inputs=date_input, outputs=s_out, ) def on_app_load(token: gr.OAuthToken | None = None, request: gr.Request | None = None): status, prefs = bootstrap_logged_in_user(token, request) logged_in = not status.startswith("Logged out") return [status, gr.update(interactive=logged_in), *prefs] demo.load( fn=on_app_load, outputs=[auth_status, run_btn, *pref_inputs], ) return demo def launch_app( host: str = "0.0.0.0", port: int = 7860, config_path: str | None = None ) -> None: if config_path: os.environ["DENSEFEED_CONFIG_PATH"] = config_path for var in ("no_proxy", "NO_PROXY"): os.environ[var] = f"{os.environ.get(var, '')},localhost,127.0.0.1".strip(",") create_ui().queue().launch(server_name=host, server_port=port)