Spaces:
Running
Running
Upload app.py
Browse files
app.py
ADDED
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
import json
|
3 |
+
import os
|
4 |
+
import xml.etree.ElementTree as ET
|
5 |
+
import openai
|
6 |
+
import edge_tts
|
7 |
+
from pydub import AudioSegment
|
8 |
+
import re
|
9 |
+
import time
|
10 |
+
import asyncio
|
11 |
+
import streamlit as st
|
12 |
+
from datetime import datetime
|
13 |
+
from dotenv import load_dotenv
|
14 |
+
|
15 |
+
load_dotenv()
|
16 |
+
|
17 |
+
PODCAST_FILE = "merged_audio.mp3"
|
18 |
+
SHOW_NOTES_FILE = "show_notes.txt"
|
19 |
+
CONVERSATION_FILE = "conversation.json"
|
20 |
+
LAST_RUN_FILE = "last_run.txt"
|
21 |
+
|
22 |
+
feed_url = "http://papers.takara.ai/api/feed"
|
23 |
+
response = requests.get(feed_url)
|
24 |
+
tree = ET.ElementTree(ET.fromstring(response.text))
|
25 |
+
|
26 |
+
daily_feed = ""
|
27 |
+
items = []
|
28 |
+
for item in tree.iter("item"):
|
29 |
+
title = item.find("title").text
|
30 |
+
link = item.find("link").text
|
31 |
+
description = item.find("description").text
|
32 |
+
items.append({"title": title, "link": link, "description": description})
|
33 |
+
daily_feed += f"Title: {title.strip()}\nDescription: {description}\n\n"
|
34 |
+
|
35 |
+
# OpenAI API client
|
36 |
+
client = openai.Client(
|
37 |
+
api_key=os.getenv("DEEPINFRA_API"),
|
38 |
+
base_url="https://api.deepinfra.com/v1/openai",
|
39 |
+
)
|
40 |
+
|
41 |
+
|
42 |
+
def build_prompt(text, items):
|
43 |
+
"""Generates a concise podcast conversation covering all research papers, ensuring correct JSON format."""
|
44 |
+
paper_summaries = "\n".join(
|
45 |
+
[f"- {item['title']} ({item['link']}): {item['description']}" for item in items]
|
46 |
+
)
|
47 |
+
|
48 |
+
# JSON Template for correct formatting
|
49 |
+
template = """
|
50 |
+
{
|
51 |
+
"conversation": [
|
52 |
+
{"speaker": "Brian", "text": ""},
|
53 |
+
{"speaker": "Jenny", "text": ""}
|
54 |
+
]
|
55 |
+
}
|
56 |
+
"""
|
57 |
+
|
58 |
+
return (
|
59 |
+
f"ποΈ Welcome to Daily Papers! Today, we're diving into the latest AI research in a quick, engaging, and "
|
60 |
+
f"informative discussion. This podcast should be **under 5 minutes**, so keep it concise while still covering "
|
61 |
+
f"the key points of each paper.\n\n"
|
62 |
+
f"Here are today's research papers:\n{paper_summaries}\n\n"
|
63 |
+
f"Convert this into a **short, engaging** podcast conversation between two experts, Brian and Jenny. "
|
64 |
+
f"Use a **natural tone, casual phrasing, and occasional filler words like 'uhm' and 'you know'** to keep it realistic. "
|
65 |
+
f"Summarize the key insights **briefly** without excessive details. Prioritize a fast-paced but informative flow. "
|
66 |
+
f"Ensure all papers are **mentioned**, but do not spend much time on each.\n\n"
|
67 |
+
f"Please follow this exact JSON format:\n{template}"
|
68 |
+
)
|
69 |
+
|
70 |
+
|
71 |
+
|
72 |
+
def extract_conversation(text, items, max_retries=3):
|
73 |
+
"""Extracts podcast conversation from OpenAI API with retries."""
|
74 |
+
for attempt in range(1, max_retries + 1):
|
75 |
+
try:
|
76 |
+
print(f"Attempt {attempt} to generate conversation...")
|
77 |
+
chat_completion = client.chat.completions.create(
|
78 |
+
messages=[{"role": "user", "content": build_prompt(text, items)}],
|
79 |
+
model="meta-llama/Llama-3.3-70B-Instruct-Turbo",
|
80 |
+
temperature=0.7,
|
81 |
+
max_tokens=4096,
|
82 |
+
)
|
83 |
+
pattern = r"\{(?:[^{}]|(?:\{[^{}]*\}))*\}"
|
84 |
+
json_match = re.search(pattern, chat_completion.choices[0].message.content)
|
85 |
+
if json_match:
|
86 |
+
return json.loads(json_match.group())
|
87 |
+
raise ValueError("No valid JSON found in response")
|
88 |
+
except Exception as e:
|
89 |
+
print(f"Error: {e}")
|
90 |
+
if attempt < max_retries:
|
91 |
+
time.sleep(2)
|
92 |
+
else:
|
93 |
+
raise RuntimeError(f"Failed after {max_retries} attempts.")
|
94 |
+
|
95 |
+
def generate_show_notes(items):
|
96 |
+
"""Creates structured show notes summarizing research papers."""
|
97 |
+
notes = "**Show Notes**\n\nIn today's episode:\n\n"
|
98 |
+
for i, item in enumerate(items):
|
99 |
+
notes += f"{i+1}. **{item['title']}**\n - {item['description']}\n - [Read More]({item['link']})\n\n"
|
100 |
+
return notes
|
101 |
+
|
102 |
+
async def generate_audio_parallel(conversation):
|
103 |
+
"""Generates audio for the podcast in parallel."""
|
104 |
+
tasks = []
|
105 |
+
audio_files = []
|
106 |
+
|
107 |
+
for i, item in enumerate(conversation["conversation"]):
|
108 |
+
text = item["text"]
|
109 |
+
output_file = f"audio_{i}.mp3"
|
110 |
+
voice = "en-US-AndrewMultilingualNeural" if item["speaker"] == "Brian" else "en-US-AvaMultilingualNeural"
|
111 |
+
|
112 |
+
# Create async TTS tasks
|
113 |
+
tasks.append(edge_tts.Communicate(text=text, voice=voice).save(output_file))
|
114 |
+
audio_files.append(output_file)
|
115 |
+
|
116 |
+
# Run all tasks concurrently
|
117 |
+
await asyncio.gather(*tasks)
|
118 |
+
return audio_files
|
119 |
+
|
120 |
+
def merge_audio_files(audio_files, output_file="merged_audio.mp3"):
|
121 |
+
"""Merges multiple audio files into one MP3 file."""
|
122 |
+
combined = AudioSegment.empty()
|
123 |
+
for file in audio_files:
|
124 |
+
audio = AudioSegment.from_file(file)
|
125 |
+
combined += audio
|
126 |
+
combined.export(output_file, format="mp3")
|
127 |
+
print(f"Merged audio saved to {output_file}")
|
128 |
+
|
129 |
+
def save_to_file(content, filename):
|
130 |
+
"""Saves content to a file."""
|
131 |
+
with open(filename, "w") as file:
|
132 |
+
file.write(content)
|
133 |
+
|
134 |
+
def load_conversation():
|
135 |
+
"""Loads conversation from file if exists."""
|
136 |
+
if os.path.exists(CONVERSATION_FILE):
|
137 |
+
with open(CONVERSATION_FILE, "r", encoding="utf-8") as file:
|
138 |
+
return json.load(file)
|
139 |
+
return None
|
140 |
+
|
141 |
+
async def generate_podcast():
|
142 |
+
"""Generates podcast content (once per day or on force generate)."""
|
143 |
+
if os.path.exists(LAST_RUN_FILE):
|
144 |
+
with open(LAST_RUN_FILE, "r") as file:
|
145 |
+
last_run_date = file.read().strip()
|
146 |
+
if last_run_date == datetime.today().strftime("%Y-%m-%d"):
|
147 |
+
print("Podcast already generated today. Skipping...")
|
148 |
+
return
|
149 |
+
|
150 |
+
conversation = extract_conversation(daily_feed, items)
|
151 |
+
save_to_file(json.dumps(conversation, indent=2), CONVERSATION_FILE)
|
152 |
+
save_to_file(generate_show_notes(items), SHOW_NOTES_FILE)
|
153 |
+
|
154 |
+
print("Generating audio...")
|
155 |
+
audio_files = await generate_audio_parallel(conversation)
|
156 |
+
merge_audio_files(audio_files)
|
157 |
+
|
158 |
+
for file in audio_files:
|
159 |
+
os.remove(file)
|
160 |
+
|
161 |
+
with open(LAST_RUN_FILE, "w") as file:
|
162 |
+
file.write(datetime.today().strftime("%Y-%m-%d"))
|
163 |
+
|
164 |
+
print("Podcast and show notes generated successfully.")
|
165 |
+
|
166 |
+
# Generate daily podcast
|
167 |
+
asyncio.run(generate_podcast())
|
168 |
+
|
169 |
+
# Streamlit UI
|
170 |
+
st.set_page_config(page_title="AI Podcast", layout="wide")
|
171 |
+
|
172 |
+
conversation_data = load_conversation()
|
173 |
+
show_notes = "**No show notes available.**"
|
174 |
+
if os.path.exists(SHOW_NOTES_FILE):
|
175 |
+
with open(SHOW_NOTES_FILE, "r", encoding="utf-8") as f:
|
176 |
+
show_notes = f.read()
|
177 |
+
|
178 |
+
st.title("ποΈ Today's AI Podcast")
|
179 |
+
|
180 |
+
col1, col2 = st.columns([2, 1])
|
181 |
+
|
182 |
+
with col1:
|
183 |
+
st.subheader("π§ Listen to the Podcast")
|
184 |
+
if os.path.exists(PODCAST_FILE):
|
185 |
+
audio_bytes = open(PODCAST_FILE, "rb").read()
|
186 |
+
st.audio(audio_bytes, format="audio/mp3")
|
187 |
+
else:
|
188 |
+
st.warning("No podcast available. Please generate an episode.")
|
189 |
+
|
190 |
+
st.subheader("π Show Notes")
|
191 |
+
st.markdown(show_notes)
|
192 |
+
|
193 |
+
with col2:
|
194 |
+
st.subheader("π¨οΈ Podcast Conversation")
|
195 |
+
if conversation_data:
|
196 |
+
for msg in conversation_data["conversation"]:
|
197 |
+
st.write(f"**{msg['speaker']}**: {msg['text']}")
|
198 |
+
else:
|
199 |
+
st.warning("No conversation data available.")
|
200 |
+
|
201 |
+
st.markdown("---")
|
202 |
+
|
203 |
+
if st.button("π Force Generate Podcast"):
|
204 |
+
asyncio.run(generate_podcast())
|
205 |
+
st.rerun()
|
206 |
+
|
207 |
+
st.markdown("π’ **Stay tuned for more AI research insights!**")
|