| """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(): |
| |
| 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], |
| ) |
|
|
| |
| 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], |
| ) |
|
|
| |
| 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, |
| ) |
|
|
| |
| 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) |
|
|