# Project export: EarlyBird

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2025
- Tagline: A personalized, interactive podcast platform powered by an AI agentic workflow and a reinforcement learning-based recommendation system.
- Devpost: https://devpost.com/software/early-bird-ujiqvh
- GitHub: https://github.com/roshanbellary/EarlyBird
- Video: https://www.youtube.com/embed/zMzrjapttIw?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — eshansinghal14 (57 commits), Roshan Bellary (56 commits), Albert (36 commits), Pulkith (13 commits)

## Devpost submission (written by the team)

### Overview

Early Bird is an AI-powered podcast generation system that curates personalized news content based on your unique interests, all sourced from a dynamically generated 3D embedding graph of current events. By leveraging state-of-the-art technologies like LangChain, Flask, and ElevenLabs, Early Bird ensures that every listener can enjoy a tailored, interactive news experience with the ability to ask questions and engage in real time. What Makes Early Bird Stand Out Personalized Experience: Through advanced AI, Early Bird offers content tailored specifically to a user's interests, ensuring they hear the most relevant news stories. Real-Time Interaction: Users can interrupt the podcast and receive dynamic, expert responses in real time, creating an engaging and interactive experience. Immersive Interface: With the 3D embedding space, users can visually navigate topics and explore related content, enhancing their connection to the material. Cutting-Edge Technology: Powered by LangChain, Flask, Perplexity Sonar, ElevenLabs, Mistral, and more, Early Bird seamlessly integrates multiple AI agents to provide an end-to-end solution for personalized podcast generation. What We Learned The power of real-time interaction: Listeners are more engaged when they can shape the content and ask follow-up questions. AI agent orchestration is crucial for creating a seamless, automated workflow that enables personalization and real-time responses. The importance of user control: Providing an immersive, interactive experience allows users to tailor the content to their specific needs and interests. Challenges We Overcame Building Interactivity: Initially, we generated static podcasts, but we quickly pivoted to an agentic system that allows for dynamic interruptions, ensuring real-time user engagement. Handling Complex Workflows: Orchestrating multiple agents in a seamless pipeline required careful design, but LangChain proved to be a powerful tool for managing the various stages of podcast creation. What's Next for Early Bird Expanding the personalization features, allowing for deeper customization of content preferences. Improving the accuracy of our research agents to provide even more insightful podcast episodes. Enhancing the interactivity by integrating more dynamic user feedback and enabling more types of user-driven interactions. How Early Bird Aligns with Sponsor Goals Zoom (Education Track Grand Prize) Early Bird redefines how we engage with educational content by curating personalized, interactive podcasts. It fosters knowledge sharing and empowers listeners to explore topics at their own pace, contributing to lifelong learning. Intersystems (Best Use of GenAI with IRIS Vector Search) By leveraging AI agents and vector embedding spaces, Early Bird uses Intersystems IRIS Vector Search to store and retrieve highly relevant content, making it an ideal fit for solutions that utilize advanced data retrieval and GenAI techniques. Context (Best AI Employee Workflow) The dynamic agentic workflow powering Early Bird, from event scraping to text-to-speech transcription, showcases the potential of AI to handle complex tasks autonomously, creating efficient systems that respond to user needs in real time. Perplexity (Best Search Hack) Early Bird is powered by Perplexity Sonar, which plays a crucial role in gathering the most relevant and up-to-date news for podcast creation. It leverages the search and reasoning capabilities of Perplexity to ensure that the content is always current and insightful. ElevenLabs (Best Use of ElevenLabs) The integration of ElevenLabs's text-to-speech technology enables Early Bird to provide a natural, human-like podcast experience. By transforming AI-generated scripts into seamless, lifelike audio, Early Bird pushes the boundaries of what's possible in AI-driven voice technologies. LangChain (Best Use of LangChain) Early Bird makes exceptional use of LangChain to manage complex workflows involving multiple agents. LangChain serves as the backbone for agent orchestration, ensuring that each step of the process—from content scraping to podcast generation—is automated and seamlessly integrated. Mistral (Best Use of Mistral AI API) Mistral's AI APIs are used to power both the Expert Agent and the Host Agent that generate the podcast scripts. Their low-latency, dynamic response capabilities are central to ensuring a smooth and interactive experience for users. Elastic (Best Use of Elasticsearch Serverless) Early Bird uses Elastic for data storage and retrieval, ensuring fast, efficient access to podcast episodes and user preferences. The integration with Elasticsearch enhances the speed and accuracy of content recommendations.

## README (from the GitHub repository)

# Early Bird: A Dynamic Podcast Generator

Our app is built with **Next.js** and **ShadCN** for the frontend, and **Flask** for the backend. We’ve implemented an agentic workflow that collects current events based on user-selected interests from a vector embedding space, and generates a personalized podcast based on these interests. The podcast generation workflow is fully automated, with each step orchestrated through LangChain.

## Workflow
1. **Event Scraping:**  
   We begin by launching a request to **Perplexity Sonar** to scrape current events based on the user’s chosen interests. The result is a list of headlines that represent the most relevant news stories.

2. **Topic Parsing:**  
   Next, we send these headlines to a **ChatGPT agent** that parses the text and categorizes the stories by topic, allowing us to organize the content effectively.

3. **Deep Research:**  
   The parsed headlines are then sent to another **Perplexity Sonar agent** for a more thorough research dive into each story, gathering additional information and context.

4. **Story Creation:**  
   The results are then sent to a **ChatGPT agent** tasked with synthesizing the information into a coherent podcast outline, setting the stage for the actual script generation.

5. **Podcast Script Generation:**  
   Two specialized **Mistral agents**, an **Expert Agent** and a **Host Agent**, then collaborate to generate the podcast script. These agents interact within the LangChain framework, ensuring a smooth, flowing conversation for the final script.

6. **Text-to-Speech:**  
   Once the script is ready, we send it to **ElevenLabs** for **text-to-speech transcription**, creating a natural-sounding audio file of the podcast.

7. **User Interaction:**  
   The generated audio is presented to the user, who can interrupt at any point to ask follow-up questions. The **Expert Agent** responds in real time, ensuring the conversation remains dynamic and interactive.

8. **3D Embedding Space:**  
   We end with a unique feature—a 3D embedding space where the user can move around to explore their interests, giving them complete control over the type of news content they want to explore in the future.

## Inspiration
Every morning, I start my day by listening to *Up First* by NPR. While I love its concise format, I often found that:
- Some stories didn’t capture my interest.
- At times, the content felt biased.
- I wished I could ask follow-up questions in real time.

These frustrations inspired us to build **Early Bird**—a dynamic podcast generator that not only curates the news you care about but also lets you interact with it.

## What it does
- Curates a personalized podcast based on your unique interests.
- Allows real-time interaction through dynamic interruptions and expert responses.
- Offers an immersive, 3D interface for exploring your interests further.

## How we built it
- **Front End:**  
  - **Next.js** with **ShadCN** for a responsive, modern user interface.
- **Search:**  
  - Integrated **Perplexity Sonar** to fetch up-to-date news based on user interests.
- **Research Distillation:**  
  - Utilized **ChatGPT** to summarize and refine the scraped content.
- **Response Generation:**  
  - Employed **Mistral** for low-latency, dynamic response generation.
- **Backend:**  
  - Built using **Flask** in Python to manage API requests and coordinate the pipeline.
- **Voice Generation:**  
  - Leveraged **11Labs** to convert scripts into natural-sounding audio.
- **Recommender Systems:**  
  - Applied reinforcement learning with **Sherman Morrison optimization** to tailor content recommendations.
- **Data Storage:**  
  - Used **Elastic** and **Intersystems** for vectorized data storage and fast retrieval of podcast episodes.

## Challenges we ran into
- **Building an Interruption System:**  
  - Initially, we generated a single MP3 file for each podcast, which made it difficult to incorporate interactivity. This led to challenges in ensuring real-time responsiveness to user questions and engagement.
- **Pivoting for Reactivity:**  
  - We quickly learned that listeners needed to interact with the content. This realization forced us to reengineer our pipeline to support dynamic interruptions and follow-up responses, ensuring the podcast remains reactive and engaging.

## Accomplishments that we're proud of
- Successfully implementing a fully agentic pipeline with LangChain for the podcast generation process.
- Creating a responsive, immersive experience that gives users control over their podcast content.
- Overcoming technical challenges to build a seamless interruption system for real-time engagement.

## What we learned
- The importance of reactivity in content delivery: Podcasts can be more engaging when listeners have the ability to interact with the content, shaping their experience in real time.
- The power of automation: By using LangChain and various AI agents, we were able to automate complex workflows, reducing manual effort and improving efficiency.
  
## What’s next for Early Bird
- Expanding the personalization options, allowing users to have even more control over the types of content they receive.
- Improving the accuracy and depth of the research agents for even more insightful, data-driven podcast episodes.
- Further enhancing the interactivity of the platform by integrating more dynamic user feedback mechanisms and content curation.


## Detected evidence (automated analysis)

Indexed codebase: 76 recognized source files, 1266 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- LangChain (technology) — detected in the code
- Mistral AI (technology) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Flask (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (98 of 98)

```
.gitignore
archive.html
backend/__init__.py
backend/config/settings.yaml
backend/podcast/__init__.py
backend/podcast/agents/__init__.py
backend/podcast/agents/audio/__init__.py
backend/podcast/agents/audio/audio_generation.py
backend/podcast/agents/audio/available_voices.py
backend/podcast/agents/audio/available_voices.txt
backend/podcast/agents/interest_classifier.py
backend/podcast/agents/perplexity.py
backend/podcast/agents/pipeline.py
backend/podcast/agents/researcher.py
backend/podcast/agents/scraper.py
backend/podcast/agents/script_generator.py
backend/podcast/agents/story_drafter.py
backend/podcast/AppData.py
backend/podcast/flask-app.py
backend/podcast/generate_graph_nodes.py
backend/podcast/global_instances.py
backend/podcast/ml/__init__.py
backend/podcast/ml/clustering.py
backend/podcast/ml/eda.ipynb
backend/podcast/ml/inference/embed.py
backend/podcast/ml/inference/fast_bandit.py
backend/podcast/ml/inference/rl_bandit_old.py
backend/podcast/ml/inference/rl_bandit.py
backend/podcast/ml/retrieval/__init__.py
backend/podcast/ml/retrieval/all_metadata_2025_01.json
backend/podcast/ml/retrieval/all_metadata_2025_02.json
backend/podcast/ml/retrieval/db/db_utils.py
backend/podcast/ml/retrieval/db/skipped.json
backend/podcast/ml/retrieval/get_all_articles_in_past.py
backend/podcast/ml/retrieval/headlines.json
backend/podcast/ml/retrieval/merger.py
backend/podcast/ml/retrieval/mock_nyt_api.json
backend/podcast/ml/retrieval/nyt_article.txt
backend/podcast/ml/retrieval/nyt_parser.py
backend/podcast/ml/retrieval/ondemand_article_data.py
backend/podcast/ml/retrieval/pull_headlines.py
backend/podcast/ml/retrieval/retrieval_pipeline.py
backend/podcast/ml/retrieval/retrieval_results.json
backend/podcast/ml/retrieval/rl_news_sampler.py
backend/podcast/ml/retrieval/run_vectorization.py
backend/podcast/ml/rl_news_sampler.py
backend/podcast/podcast.py
backend/podcast/test_pipeline.py
client/early-bird/.gitignore
client/early-bird/components.json
client/early-bird/next.config.ts
client/early-bird/package.json
client/early-bird/postcss.config.mjs
client/early-bird/README.md
client/early-bird/src/app/globals.css
client/early-bird/src/app/layout.tsx
client/early-bird/src/app/page.tsx
client/early-bird/src/app/podcast-graph/page.tsx
client/early-bird/src/app/podcast-graph/PodcastGraph.module.css
client/early-bird/src/app/podcast-view/page.tsx
client/early-bird/src/app/previous-podcasts/page.tsx
client/early-bird/src/components/ui/accordion.tsx
client/early-bird/src/components/ui/alert-dialog.tsx
client/early-bird/src/components/ui/alert.tsx
client/early-bird/src/components/ui/aspect-ratio.tsx
client/early-bird/src/components/ui/avatar.tsx
client/early-bird/src/components/ui/badge.tsx
client/early-bird/src/components/ui/button.tsx
client/early-bird/src/components/ui/calendar.tsx
client/early-bird/src/components/ui/card.tsx
client/early-bird/src/components/ui/checkbox.tsx
client/early-bird/src/components/ui/collapsible.tsx
client/early-bird/src/components/ui/command.tsx
client/early-bird/src/components/ui/context-menu.tsx
client/early-bird/src/components/ui/input.tsx
client/early-bird/src/components/ui/label.tsx
client/early-bird/src/components/ui/menubar.tsx
client/early-bird/src/components/ui/navigation-menu.tsx
client/early-bird/src/lib/utils.ts
client/early-bird/tailwind.config.ts
client/early-bird/tsconfig.json
LICENSE
package.json
README.md
requirements.txt
viz-test/.gitignore
viz-test/package.json
viz-test/public/index.html
viz-test/public/manifest.json
viz-test/public/robots.txt
viz-test/README.md
viz-test/src/App.css
viz-test/src/App.js
viz-test/src/App.test.js
viz-test/src/index.css
viz-test/src/index.js
viz-test/src/reportWebVitals.js
viz-test/src/setupTests.js
```

### Dependencies

- client/early-bird/package.json: @radix-ui/react-accordion@^1.2.3, @radix-ui/react-alert-dialog@^1.1.6, @radix-ui/react-aspect-ratio@^1.1.2, @radix-ui/react-avatar@^1.1.3, @radix-ui/react-checkbox@^1.1.4, @radix-ui/react-collapsible@^1.1.3, @radix-ui/react-context-menu@^2.2.6, @radix-ui/react-label@^2.1.2, @radix-ui/react-menubar@^1.1.6, @radix-ui/react-navigation-menu@^1.2.5, @radix-ui/react-slot@^1.1.2, @react-three/drei@^9.121.5, @react-three/fiber@^8.17.14, @types/dom-speech-recognition@^0.0.4, @types/node@^20, @types/react@^19, @types/react-dom@^19, @types/react-plotly.js@^2.6.3, @types/three@^0.173.0, class-variance-authority@^0.7.1, clsx@^2.1.1, cmdk@^1.0.4, elevenlabs@^1.51.0, elevenlabs-js@^1.2.6, framer-motion@^12.4.3, lucide-react@^0.475.0, next@^15.1.7, next-dev-https@^0.13.3, plotly.js@^3.0.0, postcss@^8, react@18.2.0, react-dom@18.2.0, react-plotly.js@^2.6.0, react-speech-recognition@^3.10.0, shadcn-ui@^0.9.4, socket.io-client@^4.8.1, tailwind-merge@^3.0.1, tailwindcss@^3.4.1, tailwindcss-animate@^1.0.7, three@^0.173.0, typescript@^5
- package.json: @react-three/drei@^9.121.5, @react-three/fiber@^8.17.14, three@^0.173.0
- requirements.txt: aiohappyeyeballs@==2.4.6, aiohttp@==3.11.12, aiosignal@==1.3.2, annotated-types@==0.7.0, anyio@==4.8.0, attrs@==25.1.0, certifi@==2025.1.31, charset-normalizer@==3.4.1, dataclasses-json@==0.6.7, distro@==1.9.0, elevenlabs@==1.51.0, eval_type_backport@==0.2.2, filelock@==3.17.0, frozenlist@==1.5.0, fsspec@==2025.2.0, h11@==0.14.0, httpcore@==1.0.7, httpx@==0.28.1, httpx-sse@==0.4.0, huggingface-hub@==0.28.1, idna@==3.10, jiter@==0.8.2, jsonpatch@==1.33, jsonpath-python@==1.0.6, jsonpointer@==3.0.0, langchain@==0.3.18, langchain-community@==0.3.17, langchain-core@==0.3.35, langchain-mistralai@==0.2.6, langchain-openai@==0.3.6, langchain-text-splitters@==0.3.6, langgraph@==0.2.72, langgraph-checkpoint@==2.0.15, langgraph-sdk@==0.1.51, langsmith@==0.3.8, marshmallow@==3.26.1, mistralai@==1.5.0, msgpack@==1.1.0, multidict@==6.1.0, mypy-extensions@==1.0.0, numpy@==1.26.4, openai@==1.63.0, orjson@==3.10.15, packaging@==24.2, propcache@==0.2.1, pydantic@==2.10.6, pydantic_core@==2.27.2, pydantic-settings@==2.7.1, pydub@==0.25.1, python-dateutil@==2.9.0.post0, python-dotenv@==1.0.1, PyYAML@==6.0.2, regex@==2024.11.6, requests@==2.32.3, requests-toolbelt@==1.0.0, six@==1.17.0, sniffio@==1.3.1, SQLAlchemy@==2.0.38, tenacity@==9.0.0, tiktoken@==0.9.0, tokenizers@==0.21.0, tqdm@==4.67.1, typing_extensions@==4.12.2, typing-inspect@==0.9.0, urllib3@==2.3.0, websockets@==14.2, yarl@==1.18.3, zstandard@==0.23.0
- viz-test/package.json: @testing-library/dom@^10.4.0, @testing-library/jest-dom@^6.6.3, @testing-library/react@^16.2.0, @testing-library/user-event@^13.5.0, react@^19.0.0, react-dom@^19.0.0, react-scripts@5.0.1, three@^0.173.0, web-vitals@^2.1.4

### Recent commits (newest first)

- Fix UI
- Fix UI
- Finish Merge
- Do Voice Questions
- generate graph nodes
- changing rl params
- changing rl params
- changing rl params
- changing rl params
- changing rl params
- Merge branch 'main' of https://github.com/roshanbellary/EarlyBird
- tweaking graph
- pushing README
- 3d plotting
- wrong npm stuff
- Fixing the three js route
- commit the changes
- fixed
- changes
- change

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "dependencies": {
    "@react-three/drei": "^9.121.5",
    "@react-three/fiber": "^8.17.14",
    "three": "^0.173.0"
  }
}

```

### requirements.txt

```
aiohappyeyeballs==2.4.6
aiohttp==3.11.12
aiosignal==1.3.2
annotated-types==0.7.0
anyio==4.8.0
attrs==25.1.0
certifi==2025.1.31
charset-normalizer==3.4.1
dataclasses-json==0.6.7
distro==1.9.0
elevenlabs==1.51.0
eval_type_backport==0.2.2
filelock==3.17.0
frozenlist==1.5.0
fsspec==2025.2.0
h11==0.14.0
httpcore==1.0.7
httpx==0.28.1
httpx-sse==0.4.0
huggingface-hub==0.28.1
idna==3.10
jiter==0.8.2
jsonpatch==1.33
jsonpath-python==1.0.6
jsonpointer==3.0.0
langchain==0.3.18
langchain-community==0.3.17
langchain-core==0.3.35
langchain-mistralai==0.2.6
langchain-openai==0.3.6
langchain-text-splitters==0.3.6
langgraph==0.2.72
langgraph-checkpoint==2.0.15
langgraph-sdk==0.1.51
langsmith==0.3.8
marshmallow==3.26.1
mistralai==1.5.0
msgpack==1.1.0
multidict==6.1.0
mypy-extensions==1.0.0
numpy==1.26.4
openai==1.63.0
orjson==3.10.15
packaging==24.2
propcache==0.2.1
pydantic==2.10.6
pydantic-settings==2.7.1
pydantic_core==2.27.2
pydub==0.25.1
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
PyYAML==6.0.2
regex==2024.11.6
requests==2.32.3
requests-toolbelt==1.0.0
six==1.17.0
sniffio==1.3.1
SQLAlchemy==2.0.38
tenacity==9.0.0
tiktoken==0.9.0
tokenizers==0.21.0
tqdm==4.67.1
typing-inspect==0.9.0
typing_extensions==4.12.2
urllib3==2.3.0
websockets==14.2
yarl==1.18.3
zstandard==0.23.0

```

### viz-test/package.json

```
{
  "name": "viz-test",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/dom": "^10.4.0",
    "@testing-library/jest-dom": "^6.6.3",
    "@testing-library/react": "^16.2.0",
    "@testing-library/user-event": "^13.5.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-scripts": "5.0.1",
    "three": "^0.173.0",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

```

### client/early-bird/package.json

```
{
  "name": "early-bird",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next-dev-https --https --qr --port 4430",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@radix-ui/react-accordion": "^1.2.3",
    "@radix-ui/react-alert-dialog": "^1.1.6",
    "@radix-ui/react-aspect-ratio": "^1.1.2",
    "@radix-ui/react-avatar": "^1.1.3",
    "@radix-ui/react-checkbox": "^1.1.4",
    "@radix-ui/react-collapsible": "^1.1.3",
    "@radix-ui/react-context-menu": "^2.2.6",
    "@radix-ui/react-label": "^2.1.2",
    "@radix-ui/react-menubar": "^1.1.6",
    "@radix-ui/react-navigation-menu": "^1.2.5",
    "@radix-ui/react-slot": "^1.1.2",
    "@react-three/drei": "^9.121.5",
    "@react-three/fiber": "^8.17.14",
    "@types/dom-speech-recognition": "^0.0.4",
    "@types/three": "^0.173.0",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "^1.0.4",
    "elevenlabs": "^1.51.0",
    "elevenlabs-js": "^1.2.6",
    "framer-motion": "^12.4.3",
    "lucide-react": "^0.475.0",
    "next": "^15.1.7",
    "next-dev-https": "^0.13.3",
    "plotly.js": "^3.0.0",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-plotly.js": "^2.6.0",
    "react-speech-recognition": "^3.10.0",
    "socket.io-client": "^4.8.1",
    "tailwind-merge": "^3.0.1",
    "tailwindcss-animate": "^1.0.7",
    "three": "^0.173.0"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "@types/react-plotly.js": "^2.6.3",
    "postcss": "^8",
    "shadcn-ui": "^0.9.4",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
}

```

### viz-test/src/index.js

```javascript
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

```

### viz-test/src/App.js

```javascript
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import './App.css';

function App() {
  const mountRef = useRef(null);
  const sceneRef = useRef(null);
  const cameraRef = useRef(null);

  useEffect(() => {
    // Scene setup
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    cameraRef.current = camera;
    sceneRef.current = scene;
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    mountRef.current.appendChild(renderer.domElement);

    // Controls
    const controls = new OrbitControls(camera, renderer.domElement);
    camera.position.z = 100;
    controls.update();

    // Generate major nodes
    const majorNodes = [];
    const majorNodeObjects = [];
    for (let i = 0; i < 50; i++) {
      majorNodes.push({
        position: new THREE.Vector3(
          (Math.random() - 0.5) * 100,
          (Math.random() - 0.5) * 100,
          (Math.random() - 0.5) * 100
        ),
        connections: []
      });
    }

    // Connect major nodes randomly
    majorNodes.forEach((node, i) => {
      const numConnections = Math.floor(Math.random() * 5) + 1;
      for (let j = 0; j < numConnections; j++) {
        const targetIndex = Math.floor(Math.random() * majorNodes.length);
        if (targetIndex !== i && !node.connections.includes(targetIndex)) {
          node.connections.push(targetIndex);
          majorNodes[targetIndex].connections.push(i);
        }
      }
    });

    // Create visual elements for major nodes
    majorNodes.forEach((node, i) => {
      const geometry = new THREE.SphereGeometry(4, 32, 32); // Increased radius to 5
      const color = new THREE.Color().setHSL(i / 50, 0.7, 0.5);
      const material = new THREE.MeshPhongMaterial({ color });
      const sphere = new THREE.Mesh(geometry, material);
      sphere.position.copy(node.position);
      scene.add(sphere);
      majorNodeObjects.push(sphere);
    });

    // Create connections between major nodes
    majorNodes.forEach((node, i) => {
      node.connections.forEach(targetIndex => {
        const geometry = new THREE.BufferGeometry().setFromPoints([
          node.position,
          majorNodes[targetIndex].position
        ]);
        const material = new THREE.LineBasicMaterial({ color: 0xaaaaaa });
        const line = new THREE.Line(geometry, material);
        scene.add(line);
      });
    });

    // Create minor nodes
    for (let i = 0; i < 500; i++) {
      const parentIndex = Math.floor(Math.random() * majorNodes.length);
      const parent = majorNodes[parentIndex];
      const distance = Math.random() * 5 + 3;
      const position = new THREE.Vector3(
        parent.position.x + (Math.random() - 0.5) * distance,
        parent.position.y + (Math.random() - 0.5) * distance,
        parent.position.z + (Math.random() - 0.5) * distance
      );

      const geometry = new THREE.SphereGeometry(1, 16, 16); // Increased radius to 1
      const material = new THREE.MeshPhongMaterial({
        color: new THREE.Color().setHSL(parentIndex / 50, 0.7, 0.7)
      });
      const sphere = new THREE.Mesh(geometry, material);
      sphere.position.copy(position);
      scene.add(sphere);
    }

    // Lighting
    const ambientLight = new THREE.AmbientLight(0x404040);
    scene.add(ambientLight);
    const light = new THREE.DirectionalLight(0xffffff, 1);
    light.position.set(10, 10, 10);
    scene.add(light);

    // Animation
    function animate() {
      requestAnimationFrame(animate);
      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    // Cleanup
    return () => {
      if (mountRef.current) {
        mountRef.current.removeChild(renderer.domElement);
      }
    };
  }, []);

  useEffect(() => {
    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2();

    function onMouseClick(event) {
      mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
      mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

      raycaster.setFromCamera(mouse, cameraRef.current);

      const intersects = raycaster.intersectObjects(sceneRef.current.children);

      if (intersects.length > 0) {
        const intersectedObject = intersects[0].object;

        if (intersectedObject instanceof THREE.Mesh) {
          intersectedObject.material.emissive.setHex(0xffffff);
          setTimeout(() => {
            intersectedObject.material.emissive.setHex(0x000000);
          }, 500);
        }
      }
    }

    window.addEventListener('click', onMouseClick);

    return () => {
      window.removeEventListener('click', onMouseClick);
    };
  }, []);

  return <div ref={mountRef} />;
}

export default App;

```

### client/early-bird/src/app/layout.tsx

```typescript
import Link from "next/link"
import Image from "next/image"
import { Inter } from "next/font/google"
import "./globals.css"
import { ReactNode } from "react";
const inter = Inter({ subsets: ["latin"] })

export const metadata = {
  title: "EarlyBird - Daily Podcast Generator",
  description: "Start your day with personalized podcasts based on your interests",
}

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <div className="flex flex-col min-h-screen">
          <header className="bg-primary text-primary-foreground shadow-md">
            <nav className="container mx-auto px-4 py-4">
              <div className="flex items-center justify-between">
                <div className="flex items-center gap-2">
                  <Image
                    src="/logo.jpg"
                    alt="EarlyBird Logo"
                    width={32}
                    height={32}
                    className="dark:invert"
                  />
                  <span className="text-xl font-bold">EarlyBird</span>
                </div>
                <ul className="flex space-x-4">
                  <li>
                    <Link href="/" className="hover:underline">
                      Home
                    </Link>
                  </li>
                  <li>
                    <Link href="/previous-podcasts" className="hover:underline">
                      Previous Podcasts
                    </Link>
                  </li>
                  <li>
                    <Link href="/podcast-graph" className="hover:underline">
                      Podcast Graph
                    </Link>
                  </li>
                </ul>
              </div>
            </nav>
          </header>
          <main className="flex-grow container mx-auto px-4 py-8">{children}</main>
          <footer className="bg-primary text-primary-foreground py-4">
            <div className="container mx-auto px-4 text-center">
              © 2025 EarlyBird. Start your day with personalized podcasts.
            </div>
          </footer>
        </div>
      </body>
    </html>
  )
}


```

### client/early-bird/src/app/page.tsx

```typescript
"use client"
import { useState, useRef, useEffect } from "react"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardHeader,
  CardTitle,
  CardDescription,
  CardContent,
} from "@/components/ui/card"
import { Loader2 } from "lucide-react"
import Image from "next/image"
import { io } from "socket.io-client"
import { motion, AnimatePresence } from "framer-motion" // Add this import

export default function Home() {
  // Pre-generated podcast file states
  const [loading, setLoading] = useState(false)
  const [files, setFiles] = useState<string[]>([])
  const [audioBlobs, setAudioBlobs] = useState<{ [key: string]: Blob }>({})
  const [currentFileIndex, setCurrentFileIndex] = useState(-1)
  const [isRecording, setIsRecording] = useState(false)
  const [isPlaying, setIsPlaying] = useState(false)
  const audioRef = useRef<HTMLAudioElement>(null)
  const mediaRecorderRef = useRef<MediaRecorder | null>(null)
  const chunksRef = useRef<Blob[]>([])

  // Podcast script state (for interactive audio)
  const [waiting, setWaiting] = useState(true)
  const [indexPlaying, setIndexPlaying] = useState(0)
  const [indexScriptIndexPlaying, setScriptIndexPlaying] = useState(0)
  const [allDone, setAllDone] = useState(false)
  const [dataJson, setDataJson] = useState<any>(null)

  // Refs to hold current state values for asynchronous loops.
  const dataJsonRef = useRef<any>(null)
  const indexPlayingRef = useRef(indexPlaying)
  const indexScriptIndexPlayingRef = useRef(indexScriptIndexPlaying)
  const waitingRef = useRef(waiting)
  const allDoneRef = useRef(allDone)

  // --- NEW: Interrupt State --- 
  // This state flag will be true while we are processing an interrupt.
  const [isInterrupting, setIsInterrupting] = useState(false)
  const isInterruptingRef = useRef(isInterrupting)
  useEffect(() => {
    isInterruptingRef.current = isInterrupting
  }, [isInterrupting])
  // --------------------------------

  // This flag ensures only one podcast loop runs at a time.
  const playbackLoopActiveRef = useRef(false)

  // This ref is used for interactive ElevenLabs audio playback.
  const currentAudioRef = useRef<HTMLAudioElement | null>(null)
  const recognitionRef = useRef<SpeechRecognition | null>(null)

  // Keep our refs in sync with state.
  useEffect(() => {
    dataJsonRef.current = dataJson
  }, [dataJson])
  useEffect(() => {
    indexPlayingRef.current = indexPlaying
  }, [indexPlaying])
  useEffect(() => {
    indexScriptIndexPlayingRef.current = indexScriptIndexPlaying
  }, [indexScriptIndexPlaying])
  useEffect(() => {
    waitingRef.current = waiting
  }, [waiting])
  useEffect(() => {
    allDoneRef.current = allDone
  }, [allDone])

  const [alltext, setAlltext] = useState([])

  // --- SOCKET CONNECTION ---
  useEffect(() => {
    const socket = io("http://localhost:8000")
    socket.on("connect", () => {
      console.log("Connected to WebSocket server")
    })
    socket.on("my_response", (data) => {
      console.log("Received message:", data)
      if (data?.data?.data) {
        setDataJson(data)
      }
    })
    socket.on("disconnect", () => {
      console.log("Disconnected from WebSocket server")
    })
    return () => {
      socket.disconnect()
    }
  }, [])

  // VOICE IDs
  const host_voice = "9BWtsMINqrJLrRacOk9x"
  const guest_voice = "CwhRBWXzGAHq8TQ4Fs17"

  /**
   * Stops any playing audio—both the interactive (ElevenLabs) audio and the pre‐generated <audio> element.
   */
  function stopAllAudio() {
    if (currentAudioRef.current) {
      currentAudioRef.current.pause()
      currentAudioRef.current = null
    }
    if (audioRef.current) {
      audioRef.current.pause()
      audioRef.current.currentTime = 0
    }
  }

  /**
   * Plays text using the ElevenLabs API.
   * If an interrupt is active (checked via isInterruptingRef), the promise immediately resolves false.
   */
  const playAudioAndWait = async (text: string, voice: string) => {
    setAlltext((prev) => [...prev, text])
    return new Promise(async (resolve) => {
      if (isInterruptingRef.current) {
        return resolve(false)
      }
      try {
        const response = await fetch(
          "https://api.elevenlabs.io/v1/text-to-speech/" + voice,
          {
            method: "POST",
            headers: {
              accept: "audio/mpeg",
              "xi-api-key":
                "sk_3a0d683894df57a344b44c211e6f49ecf0a5eae43c003342",
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              text,
              model_id: "eleven_multilingual_v2",
              voice_settings: {
                stability: 0.5,
                similarity_boost: 0.75,
                style: 0.0,
                use_speaker_boost: true,
              },
            }),
          }
        )
        const audioBlob = await response.blob()
        const audio = new Audio()
        currentAudioRef.current = audio
        audio.src = URL.createObjectURL(audioBlob)
        audio.onended = () => {
          currentAudioRef.current = null
          resolve(true)
        }
        audio.play()
        const checkInterruptInterval = setInterval(() => {
          if (isInterruptingRef.current) {
            audio.pause()
            clearInterval(checkInterruptInterval)
            currentAudioRef.current = null
            resolve(false)
          }
        }, 100)
      } catch (error) {
        console.error("Error playing audio:", error)
        resolve(false)
      }
    })
  }

  async function genNext(index: number) {
    await fetch("http://localhost:8000/generate_next", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ next_id: index }),
    })
  }

  /**
   * Main podcast playback loop.
   * It exits immediately if isInterrupting is true.
   * Indexes are advanced only if no interrupt occurs.
   */
  async function continuePlayingPodcast() {
    if (playbackLoopActiveRef.current) retu
[truncated — 13637 more characters]
```

### client/early-bird/src/app/previous-podcasts/page.tsx

```typescript
"use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { useEffect, useState } from "react"

const PreviousPodcasts = () => {
  const [previousPodcasts, setPreviousPodcasts] = useState<{ title: string, date: string, topic: string }[]>([])

  useEffect(() => {
    const fetchPodcasts = async () => {
      try {
        const response = await fetch("http://localhost:8000/get/transcripts")
        const data = await response.json();
        console.log(data);
        let podcasts = []
        for (const podcast of data["metadata"]["metadata"]) {
            const date = new Date(podcast.datetime);
            const formattedDate = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
            const formattedTopic = `${podcast.stories[0].topic} \n ${podcast.stories[1].topic} \n ${podcast.stories[2].topic}`;
            podcasts.push({
            title: formattedDate,
            date: podcast.datetime,
            topic: formattedTopic,
            })
        }
        setPreviousPodcasts(podcasts)
      } catch (error) {
        console.error("Error fetching podcasts:", error)
      }
    }

    fetchPodcasts()
  }, [])

  return (
    <div className="space-y-6">
      <h1 className="text-3xl font-bold">Previous EarlyBird Episodes</h1>
      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
        {previousPodcasts.map((podcast) => (
          <Card key={podcast.title + podcast.topic}>
            <CardHeader>
              <CardTitle>{podcast.title}</CardTitle>
            </CardHeader>
            <CardContent>
              <ul className="list-disc pl-5 space-y-1">
              {podcast.topic.split('\n').map((topic, index) => (
                <li key={index}>{topic}</li>
              ))}
              </ul>
            </CardContent>
          </Card>
        ))}
      </div>
    </div>
  )
}

export default PreviousPodcasts


```

### client/early-bird/src/app/podcast-view/page.tsx

```typescript
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';

interface PodcastMetadata {
    title: string;
    description: string;
    author: string;
    date: string;
}

const PodcastViewPage = () => {
    const router = useRouter();
    const { id } = router.query;
    const [audioSrc, setAudioSrc] = useState<string | null>(null);
    const [metadata, setMetadata] = useState<PodcastMetadata | null>(null);

    useEffect(() => {
        if (id) {
            // Fetch audio file
            fetch(`/api/podcast/audio/${id}`)
                .then(response => response.json())
                .then(data => {
                    setAudioSrc(data.audioUrl);
                })
                .catch(error => {
                    console.error('Error fetching audio file:', error);
                });

            // Fetch metadata
            fetch(`/api/podcast/metadata/${id}`)
                .then(response => response.json())
                .then(data => {
                    setMetadata(data);
                })
                .catch(error => {
                    console.error('Error fetching metadata:', error);
                });
        }
    }, [id]);

    if (!id) {
        return <div>Loading...</div>;
    }

    return (
        <div>
            {metadata && (
                <div>
                    <h1>{metadata.title}</h1>
                    <p>{metadata.description}</p>
                    <p><strong>Author:</strong> {metadata.author}</p>
                    <p><strong>Date:</strong> {metadata.date}</p>
                </div>
            )}
            {audioSrc ? (
                <audio controls>
                    <source src={audioSrc} type="audio/mpeg" />
                    Your browser does not support the audio element.
                </audio>
            ) : (
                <p>Loading audio...</p>
            )}
        </div>
    );
};

export default PodcastViewPage;
```

[60 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]