# Project export: Mappit! - A Speech to Diagram Mind Mapper

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: CruzHacks 2026
- Tagline: Given an audio file with speech, Mappit generates a mind map sorting concepts into a clear and understandable layout. Mappit's interactive UI allows users to modify the maps, and build their ideas.
- Devpost: https://devpost.com/software/mappit-a-speech-to-diagram-mind-mapper
- GitHub: https://github.com/sohamgarg2020/MindMap
- Video: https://www.youtube.com/embed/2kMaPXGpZf8?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Soham Garg (14 commits), dylankprice (10 commits), Abhishek-Adari (2 commits)

## Devpost submission (written by the team)

### Inspiration

Information today is increasingly consumed through audio—lectures, meetings, podcasts, and interviews—but audio is inherently linear and difficult to revisit. We were inspired by the question: what if spoken ideas could be transformed into structured, visual workflows automatically? We wanted to help students, researchers, and creators turn dense audio into something explorable, skimmable, and actionable.

### What it does

Mappit! takes an audio file as input and automatically generates a visual flowchart representing the key ideas, concepts, and relationships discussed in the audio. Instead of reading long transcripts, users can explore a structured graph that highlights major themes while still preserving supporting details.

### How we built it

We designed Mappit! as a modular pipeline powered by AI agents: Speech-to-text using Whisper to generate accurate transcripts Concept extraction agents that identify topics, subtopics, and relationships Node generation agents that create raw graph nodes from extracted concepts Cleaning and filtering logic to reduce noise and organize the graph Frontend visualization using an interactive flowchart interface to display nodes and edges Each agent operates independently, acting like a function in the pipeline. This made the system easier to debug, extend, and iterate on under hackathon constraints.

### Challenges we ran into

One of our biggest challenges was overpopulation of the graph. Audio naturally contains many small, niche ideas, and naïvely converting them into nodes resulted in cluttered and overwhelming visualizations. Balancing completeness with clarity—deciding what concepts truly mattered—was a nontrivial problem. We also faced challenges in designing layouts that felt intuitive rather than chaotic, especially as the number of nodes increased.

### Accomplishments we're proud of

Successfully transforming raw audio into a structured visual representation Building a fully modular, agent-based pipeline that can be extended easily Creating an interactive flowchart that makes long-form audio more digestible Tackling a real usability problem rather than just a technical one

### What we learned

We learned that visual clarity is just as important—if not more—than model accuracy. Extracting information is easier than organizing it in a way that humans actually want to explore. We also gained valuable experience working with multi-agent AI systems, designing pipelines under time pressure, and iterating quickly based on visual feedback.

### What's next

for Mappit! Introduce hierarchical abstraction, grouping minor nodes under higher-level concepts Add importance scoring so only the most relevant ideas appear by default Improve graph layout and clustering for readability Improve connections between nodes Enable live usage, such as live streams or real-time video conferencing

## README (from the GitHub repository)

# Mappit!

## Inspiration
Information today is increasingly consumed through audio—lectures, meetings, podcasts, and interviews—but audio is inherently linear and difficult to revisit. We were inspired by the question: **what if spoken ideas could be transformed into structured, visual workflows automatically?**  
We wanted to help students, researchers, and creators turn dense audio into something explorable, skimmable, and actionable.


## What it does
**Mappit!** takes an audio file as input and automatically generates a visual flowchart representing the key ideas, concepts, and relationships discussed in the audio. Instead of reading long transcripts, users can explore a structured graph that highlights major themes while still preserving supporting details.


## How we built it
We designed **Mappit!** as a modular pipeline powered by AI agents:
- **Speech-to-text** using Whisper to generate accurate transcripts  
- **Concept extraction agents** that identify topics, subtopics, and relationships  
- **Node generation agents** that create raw graph nodes from extracted concepts  
- **Cleaning and filtering logic** to reduce noise and organize the graph  
- **Frontend visualization** using an interactive flowchart interface to display nodes and edges  
Each agent operates independently, acting like a function in the pipeline. This made the system easier to debug, extend, and iterate on under hackathon constraints.


## Setup

### Prerequisites
- **Node.js** (v14 or higher) and npm
- **Python** (v3.8 or higher) and pip
- **FFmpeg** (required for audio processing)

### Installation

#### 1. Clone the repository
```bash
git clone https://github.com/yourusername/mappit.git
cd mappit
```

#### 2. Backend Setup
```bash
cd backend
pip install -r requirements.txt
```

#### 3. Frontend Setup
```bash
cd frontend
npm install
```

#### 4. Environment Variables
Create a `.env` file in the `backend` directory with the following variables:
```bash
OPENAI_API_KEY=your_openai_api_key_here
ANTHROPIC_API_KEY=your_anthropic_api_key_here  # if using Claude
```

#### 5. Run the Application

**Start the backend server:**
```bash
cd backend
python api.py
```

**In a new terminal, start the frontend:**
```bash
cd frontend
npm start
```

The app should now be running at `http://localhost:3000`


## What's next for Mappit!
- Introduce **hierarchical abstraction**, grouping minor nodes under higher-level concepts  
- Add **importance scoring** so only the most relevant ideas appear by default  
- Improve **graph layout and clustering** for readability  
- Improve **connections between nodes**  
- Enable **live usage**, such as live streams or real-time video conferencing


## Detected evidence (automated analysis)

Indexed codebase: 17 recognized source files, 65 KB.
- CSS (language) — detected in the code
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- LangChain (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- PyTorch (technology) — detected in the code
- React (technology) — detected in the code

## Codebase structure (from repository index)

### Files (25 of 25)

```
.DS_Store
.gitignore
backend/.DS_Store
backend/agents/__init__.py
backend/agents/concept_agent.py
backend/agents/dependency_agent.py
backend/agents/validator_agent.py
backend/api.py
backend/audio_detection.py
backend/main.py
backend/requirements.txt
frontend/.gitignore
frontend/package.json
frontend/public/index.html
frontend/public/manifest.json
frontend/public/robots.txt
frontend/README.md
frontend/src/App.css
frontend/src/App.jsx
frontend/src/App.test.js
frontend/src/index.css
frontend/src/index.js
frontend/src/reportWebVitals.js
frontend/src/setupTests.js
README.md
```

### Dependencies

- backend/requirements.txt: flask@==3.1.2, flask-cors@==6.0.2, langchain@==0.3.27, langchain-anthropic@==0.3.22, langchain-community@==0.3.31, langchain-core@==0.3.79, langchain-openai@==0.3.35, langchain-text-splitters@==0.3.11, langsmith@==0.4.34, numpy@==2.2.6, openai@>=1.104.2,<3.0.0, openai-whisper@==20250625, pydub@==0.25.1, python-dotenv@==1.0.1, regex@==2025.9.18, tiktoken@==0.12.0, torch@==2.9.1, tqdm@==4.67.1, werkzeug@==3.1.5
- frontend/package.json: @testing-library/dom@^10.4.1, @testing-library/jest-dom@^6.9.1, @testing-library/react@^16.3.1, @testing-library/user-event@^13.5.0, react@^19.2.3, react-dom@^19.2.3, react-scripts@5.0.1, reactflow@^11.11.4, web-vitals@^2.1.4

### Recent commits (newest first)

- Update README.md
- Update README.md
- Update README.md
- Update README.md
- Update README.md
- Name Finalization
- Ignore local requirements file
- Ignore cache and local uploads
- UI Final
- Fixed up generating concepts
- fixed
- requirements
- Requirements
- Updated backend and frontend connection
- siderbar
- Backend
- Delete .gitignore
- Delete frontend directory
- Delete lecture-flowchart-ai directory
- Frontend

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

### backend/requirements.txt

```
# Web server
flask==3.1.2
flask-cors==6.0.2
werkzeug==3.1.5
python-dotenv==1.0.1

# OpenAI + LangChain stack
openai>=1.104.2,<3.0.0
langchain==0.3.27
langchain-core==0.3.79
langchain-community==0.3.31
langchain-openai==0.3.35
langchain-anthropic==0.3.22
langchain-text-splitters==0.3.11
langsmith==0.4.34

# Audio / ML
openai-whisper==20250625
torch==2.9.1
numpy==2.2.6
tqdm==4.67.1
pydub==0.25.1
regex==2025.9.18
tiktoken==0.12.0

```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/dom": "^10.4.1",
    "@testing-library/jest-dom": "^6.9.1",
    "@testing-library/react": "^16.3.1",
    "@testing-library/user-event": "^13.5.0",
    "react": "^19.2.3",
    "react-dom": "^19.2.3",
    "react-scripts": "5.0.1",
    "reactflow": "^11.11.4",
    "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"
    ]
  }
}

```

### backend/main.py

```python
from audio_detection import audio_to_text
from agents.concept_agent import extract_concepts
from agents.dependency_agent import (
    extract_dependencies, 
    extract_conceptual_dependencies,
    ensure_full_connectivity
)
from agents.validator_agent import validate_edges


def chunk_text(text: str, chunk_size: int = 3000, overlap: int = 500):
    """
    Split text into overlapping chunks by character count.
    
    Args:
        text: The text to chunk
        chunk_size: Target size of each chunk in characters
        overlap: Number of characters to overlap between chunks
    """
    chunks = []
    start = 0
    
    while start < len(text):
        end = start + chunk_size
        
        # If this isn't the last chunk, try to break at a sentence
        if end < len(text):
            # Look for sentence endings in the last 200 chars of the chunk
            search_start = max(start, end - 200)
            last_period = text.rfind('.', search_start, end)
            last_question = text.rfind('?', search_start, end)
            last_exclamation = text.rfind('!', search_start, end)
            
            best_break = max(last_period, last_question, last_exclamation)
            if best_break > start:
                end = best_break + 1
        
        chunks.append(text[start:end])
        start = end - overlap  # Move back by overlap amount
        
    return chunks



def build_lecture_graph(audio_path: str):
    print("\n[1] Transcribing audio...")
    lecture_text = audio_to_text(audio_path)

    if not lecture_text or len(lecture_text.strip()) == 0:
        raise ValueError("Transcription failed or returned empty text.")

    print(f"Transcript length: {len(lecture_text)} characters")

    print("\n[2] Chunking lecture text...")
    chunks = chunk_text(lecture_text, chunk_size=4000, overlap=500)
    print(f"Created {len(chunks)} chunks")

    print("\n[3] Extracting concepts per chunk...")
    concepts = []
    seen_labels = {}
    
    for i, chunk in enumerate(chunks):
        print(f"  Processing chunk {i+1}/{len(chunks)}...")
        chunk_concepts = extract_concepts(chunk)
        
        for c in chunk_concepts:
            label_key = c["label"].lower().strip()
            normalized = label_key.replace("the ", "").replace("'s ", " ")
            
            is_duplicate = False
            for existing_label in seen_labels.keys():
                words1 = set(normalized.split())
                words2 = set(existing_label.split())
                if len(words1 & words2) / max(len(words1), len(words2)) > 0.8:
                    is_duplicate = True
                    break
            
            if not is_duplicate:
                c["id"] = f"C{len(concepts) + 1}"
                concepts.append(c)
                seen_labels[normalized] = c["id"]
        
        print(f"    Found {len(chunk_concepts)} concepts, {len(concepts)} total unique")

    if not concepts:
        raise ValueError("No concepts extracted after chunking.")

    print(f"\n[4] Extracted {len(concepts)} unique concepts total")
    
    # Print popularity distribution
    pop_dist = {}
    for c in concepts:
        pop = c.get("popularity", 3)
        pop_dist[pop] = pop_dist.get(pop, 0) + 1
    print(f"  Popularity distribution: {dict(sorted(pop_dist.items()))}")

    # IMPROVED: Multi-pass edge extraction with popularity awareness
    print("\n[5] Extracting dependencies (popularity-aware)...")
    all_edges = []
    
    # Pass 1: Thematic relationships
    print("  Pass 1: Thematic relationships...")
    thematic_edges = extract_dependencies(
        concepts, 
        lecture_text[:8000],
        focus="thematic"
    )
    all_edges.extend(thematic_edges)
    print(f"    Found {len(thematic_edges)} thematic edges")
    
    # Pass 2: Concept-to-concept relationships
    print("  Pass 2: Concept interdependencies...")
    concept_edges = extract_conceptual_dependencies(concepts)
    all_edges.extend(concept_edges)
    print(f"    Found {len(concept_edges)} conceptual edges")
    
    # Validate intermediate edges
    print("  Validating intermediate edges...")
    validated_edges = validate_edges(all_edges, concepts)
    print(f"    Valid edges after initial passes: {len(validated_edges)}")
    
    # Pass 3: Ensure full connectivity
    print("  Pass 3: Ensuring all concepts are connected...")
    connectivity_edges = ensure_full_connectivity(concepts, validated_edges)
    all_edges.extend(connectivity_edges)
    
    print(f"  Total raw edges: {len(all_edges)}")

    print("\n[6] Final validation...")
    edges = validate_edges(all_edges, concepts)
    print(f"Final validated edges: {len(edges)}")
    
    # Verify connectivity
    connected_ids = set()
    for edge in edges:
        connected_ids.add(edge.get("from"))
        connected_ids.add(edge.get("to"))
    
    all_ids = {c["id"] for c in concepts}
    still_isolated = all_ids - connected_ids
    
    if still_isolated:
        print(f"  ⚠️  Warning: {len(still_isolated)} concepts still isolated: {sorted(still_isolated)}")
    else:
        print(f"  ✓ All {len(concepts)} concepts are connected!")
    
    # Print edge statistics by popularity
    print("\n  Edge statistics by popularity:")
    edge_counts = {c["id"]: 0 for c in concepts}
    for edge in edges:
        edge_counts[edge["from"]] += 1
        edge_counts[edge["to"]] += 1
    
    for pop in [5, 4, 3, 2, 1]:
        pop_concepts = [c for c in concepts if c.get("popularity") == pop]
        if pop_concepts:
            avg_edges = sum(edge_counts[c["id"]] for c in pop_concepts) / len(pop_concepts)
            print(f"    Popularity {pop}: {len(pop_concepts)} concepts, avg {avg_edges:.1f} edges each")

    return {
        "concepts": concepts,
        "edges": edges
    }


if __name__ == "__main__":
    AUDIO_FILE = "videoplayback.mp4"

    try:
        graph = build_lecture_graph(AUDIO_FILE)

        print("\n=== FINAL GRAPH SUMMARY ===")
        print(f"T
[truncated — 939 more characters]
```

### frontend/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();

```

### frontend/src/App.jsx

```javascript
import React, { useRef, useState, useCallback, useEffect } from 'react';
import ReactFlow, {
  Background,
  Controls,
  useNodesState,
  useEdgesState,
  addEdge,
  Handle,
  Position,
} from 'reactflow';
import 'reactflow/dist/style.css';

const API_BASE = 'http://localhost:5000/api';

// Custom node component with dynamic sizing and circular shape
const CustomNode = ({ data, id }) => {
  const connectionCount = data.connectionCount || 0;
  let size = 120;
  let fontSize = '14px';
  let padding = '20px';
  
  if (connectionCount >= 5) {
    size = 180;
    fontSize = '20px';
    padding = '40px';
  } else if (connectionCount >= 3) {
    size = 150;
    fontSize = '17px';
    padding = '30px';
  } else if (connectionCount >= 1) {
    size = 120;
    fontSize = '14px';
    padding = '20px';
  } else {
    size = 100;
    fontSize = '12px';
    padding = '16px';
  }
  
  const colors = [
    '#2563eb', '#10b981', '#f59e0b', '#06b6d4', 
    '#8b5cf6', '#ec4899', '#ef4444', '#14b8a6', '#f97316',
  ];
  
  const colorIndex = (connectionCount + data.label.length) % colors.length;
  const backgroundColor = data.isRoot ? '#5b7ee5' : colors[colorIndex];
  const textColor = 'white';
  const borderColor = 'rgba(255,255,255,0.3)';
  
  const isHighlighted = data.isHighlighted;
  const isFaded = data.isFaded;
  const opacity = isFaded ? 0.2 : 1;
  
  const handleStyle = {
    background: '#60a5fa',
    width: '12px',
    height: '12px',
    border: '2px solid white',
    opacity: 1,
  };
  
  return (
    <div style={{
      background: backgroundColor,
      color: textColor,
      borderRadius: '50%',
      width: `${size}px`,
      height: `${size}px`,
      border: `2px solid ${borderColor}`,
      boxShadow: connectionCount >= 3 ? '0 8px 24px rgba(0,0,0,0.2)' : '0 4px 12px rgba(0,0,0,0.1)',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      padding: padding,
      transition: 'all 0.3s ease',
      position: 'relative',
      opacity: opacity,
      transform: isHighlighted ? 'scale(1.1)' : 'scale(1)',
    }}>
      <Handle type="target" position={Position.Left} id="left" style={handleStyle} />
      <Handle type="target" position={Position.Top} id="top" style={handleStyle} />
      <Handle type="target" position={Position.Right} id="right" style={handleStyle} />
      <Handle type="target" position={Position.Bottom} id="bottom" style={handleStyle} />
      
      <div style={{ 
        fontWeight: connectionCount >= 3 ? '700' : '600', 
        fontSize: fontSize, 
        lineHeight: '1.3',
        wordWrap: 'break-word',
        textAlign: 'center',
        maxWidth: '100%',
        fontFamily: 'system-ui, "Segoe UI", Tahoma, Arial, sans-serif',
      }}>
        {data.label}
      </div>
      
      <Handle type="source" position={Position.Left} id="left" style={handleStyle} />
      <Handle type="source" position={Position.Top} id="top" style={handleStyle} />
      <Handle type="source" position={Position.Right} id="right" style={handleStyle} />
      <Handle type="source" position={Position.Bottom} id="bottom" style={handleStyle} />
    </div>
  );
};

const nodeTypes = {
  custom: CustomNode,
};

export default function App() {
  const [nodes, setNodes, onNodesChange] = useNodesState([]);
  const [edges, setEdges, onEdgesChange] = useEdgesState([]);
  const [isProcessing, setIsProcessing] = useState(false);
  const [status, setStatus] = useState('');
  const [selectedNode, setSelectedNode] = useState(null);
  const [highlightedNodeId, setHighlightedNodeId] = useState(null);
  const [showConfirmClear, setShowConfirmClear] = useState(false);

  const fileInputRef = useRef(null);

  const onConnect = (params) => {
    const isDuplicate = edges.some(edge => 
      (edge.source === params.source && edge.target === params.target) ||
      (edge.source === params.target && edge.target === params.source)
    );
    
    if (isDuplicate) return;
    
    setEdges((eds) => addEdge({ 
      ...params, 
      type: 'smoothstep',
      animated: false,
      markerEnd: {
        type: 'arrowclosed',
        color: '#94a3b8',
        width: 18,
        height: 18,
      },
      style: { 
        strokeWidth: 2.5, 
        stroke: '#94a3b8',
      },
    }, eds));
    
    setNodes((nds) =>
      nds.map((node) => {
        if (node.id === params.source || node.id === params.target) {
          const newCount = (node.data.connectionCount || 0) + 1;
          return {
            ...node,
            data: { ...node.data, connectionCount: newCount }
          };
        }
        return node;
      })
    );
  };

  const onNodeClick = useCallback((event, node) => {
    setSelectedNode(node);
    setHighlightedNodeId(node.id);
  }, []);

  const onPaneClick = useCallback(() => {
    setSelectedNode(null);
    setHighlightedNodeId(null);
  }, []);

  const deleteSelectedNode = useCallback(() => {
    if (!selectedNode) return;
    
    const connectedEdges = edges.filter(
      (edge) => edge.source === selectedNode.id || edge.target === selectedNode.id
    );
    
    setNodes((nds) => {
      const remainingNodes = nds.filter((node) => node.id !== selectedNode.id);
      
      return remainingNodes.map((node) => {
        const wasConnected = connectedEdges.some(
          (edge) => edge.source === node.id || edge.target === node.id
        );
        
        if (wasConnected) {
          const newCount = Math.max(0, (node.data.connectionCount || 0) - 1);
          return {
            ...node,
            data: { ...node.data, connectionCount: newCount }
          };
        }
        return node;
      });
    });
    
    setEdges((eds) => 
      eds.filter((edge) => edge.source !== selectedNode.id && edge.target !== selectedNode.id)
    );
    
    setSelectedNode(null);
    setHighlightedNodeId(null);
  }, [selectedNode, edges, setNodes, setEdges]);

  useEffect(() => {
    const handleKeyDown = (e) => {
      if ((e.key === 'Backspace' || e.key === 'Delete
[truncated — 20967 more characters]
```

### backend/audio_detection.py

```python
import whisper

def audio_to_text(path, model_size="base"):
    """
    Audio file → structured transcript using Whisper
    """
    model = whisper.load_model(model_size)

    result = model.transcribe(path)

    segments = ""
    for seg in result["segments"]:
        segments += f"[{round(seg['start'], 2)}–{round(seg['end'], 2)}] {seg['text'].strip()} \n"

    return segments


if __name__ == "__main__":
    segments = audio_to_text(
        "Literature of C.S. Lewis - 01.mp3"
    )

    print(segments)



```

### backend/api.py

```python
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS
from werkzeug.utils import secure_filename
import os
import json
from pathlib import Path

from audio_detection import audio_to_text
from agents.concept_agent import extract_concepts
from agents.dependency_agent import (
    extract_dependencies, 
    extract_conceptual_dependencies,
    ensure_full_connectivity
)
from agents.validator_agent import validate_edges

app = Flask(__name__, static_folder='build', static_url_path='')

# Simple CORS - allow all origins for development
CORS(app, origins="*", supports_credentials=False)

# Configuration
UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'mp3', 'mp4', 'wav', 'ogg', 'm4a', 'flac'}
Path(UPLOAD_FOLDER).mkdir(exist_ok=True)

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024  # 500MB max file size

# Store the latest graph in memory
current_graph = {
    "concepts": [],
    "edges": []
}


def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


def chunk_text(text: str, chunk_size: int = 3000, overlap: int = 500):
    """Split text into overlapping chunks by character count."""
    chunks = []
    start = 0
    
    while start < len(text):
        end = start + chunk_size
        
        if end < len(text):
            search_start = max(start, end - 200)
            last_period = text.rfind('.', search_start, end)
            last_question = text.rfind('?', search_start, end)
            last_exclamation = text.rfind('!', search_start, end)
            
            best_break = max(last_period, last_question, last_exclamation)
            if best_break > start:
                end = best_break + 1
        
        chunks.append(text[start:end])
        start = end - overlap
        
    return chunks


def build_lecture_graph(audio_path: str):
    """Build the concept graph from audio file."""
    print("\n[1] Transcribing audio...")
    lecture_text = audio_to_text(audio_path)

    if not lecture_text or len(lecture_text.strip()) == 0:
        raise ValueError("Transcription failed or returned empty text.")

    print(f"Transcript length: {len(lecture_text)} characters")

    print("\n[2] Chunking lecture text...")
    chunks = chunk_text(lecture_text, chunk_size=4000, overlap=500)
    print(f"Created {len(chunks)} chunks")

    print("\n[3] Extracting concepts per chunk...")
    concepts = []
    seen_labels = {}
    
    for i, chunk in enumerate(chunks):
        print(f"  Processing chunk {i+1}/{len(chunks)}...")
        chunk_concepts = extract_concepts(chunk)
        
        for c in chunk_concepts:
            label_key = c["label"].lower().strip()
            normalized = label_key.replace("the ", "").replace("'s ", " ")
            
            is_duplicate = False
            for existing_label in seen_labels.keys():
                words1 = set(normalized.split())
                words2 = set(existing_label.split())
                if len(words1 & words2) / max(len(words1), len(words2)) > 0.8:
                    is_duplicate = True
                    break
            
            if not is_duplicate:
                c["id"] = f"C{len(concepts) + 1}"
                concepts.append(c)
                seen_labels[normalized] = c["id"]
        
        print(f"    Found {len(chunk_concepts)} concepts, {len(concepts)} total unique")

    if not concepts:
        raise ValueError("No concepts extracted after chunking.")

    print(f"\n[4] Extracted {len(concepts)} unique concepts total")
    
    pop_dist = {}
    for c in concepts:
        pop = c.get("popularity", 3)
        pop_dist[pop] = pop_dist.get(pop, 0) + 1
    print(f"  Popularity distribution: {dict(sorted(pop_dist.items()))}")

    print("\n[5] Extracting dependencies (popularity-aware)...")
    all_edges = []
    
    print("  Pass 1: Thematic relationships...")
    thematic_edges = extract_dependencies(concepts, lecture_text[:8000], focus="thematic")
    all_edges.extend(thematic_edges)
    print(f"    Found {len(thematic_edges)} thematic edges")
    
    print("  Pass 2: Concept interdependencies...")
    concept_edges = extract_conceptual_dependencies(concepts)
    all_edges.extend(concept_edges)
    print(f"    Found {len(concept_edges)} conceptual edges")
    
    print("  Validating intermediate edges...")
    validated_edges = validate_edges(all_edges, concepts)
    print(f"    Valid edges after initial passes: {len(validated_edges)}")
    
    print("  Pass 3: Ensuring all concepts are connected...")
    connectivity_edges = ensure_full_connectivity(concepts, validated_edges)
    all_edges.extend(connectivity_edges)
    
    print(f"  Total raw edges: {len(all_edges)}")

    print("\n[6] Final validation...")
    edges = validate_edges(all_edges, concepts)
    print(f"Final validated edges: {len(edges)}")

    return {
        "concepts": concepts,
        "edges": edges
    }


# API Routes

@app.route('/')
def serve_react():
    """Serve the React app."""
    return send_from_directory(app.static_folder, 'index.html')


@app.route('/api/mindmap-data', methods=['GET'])
def get_mindmap_data():
    """Get the current mindmap data."""
    return jsonify(current_graph)


@app.route('/api/upload-audio', methods=['POST'])
def upload_audio():
    """Handle audio file upload and process it."""
    global current_graph
    
    if 'audio' not in request.files:
        return jsonify({'error': 'No audio file provided'}), 400
    
    file = request.files['audio']
    
    if file.filename == '':
        return jsonify({'error': 'No file selected'}), 400
    
    if not allowed_file(file.filename):
        return jsonify({'error': 'Invalid file type. Allowed: mp3, mp4, wav, ogg, m4a, flac'}), 400
    
    try:
        # Save the uploaded file
        filename = secure_filename(file.filename)
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
      
[truncated — 2013 more characters]
```

### backend/agents/__init__.py

```python


```

### frontend/src/setupTests.js

```javascript
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

```

### frontend/src/App.test.js

```javascript
import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});

```

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