DenseFeed / src /densefeed /app.py
j14i's picture
add history latest podcast download
b9788b9 verified
Raw
History Blame Contribute Delete
8.75 kB
"""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)