Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,9 @@ coverage/
# Misc
*.tsbuildinfo
next-env.d.ts

backend/.env
backend/transcripts.db-journal
backend/__pycache__/
backend/__pycache__/
backend/audio_transcripts/dear_man_role_play_analysis.json
1 change: 1 addition & 0 deletions backend/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
51 changes: 51 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Mediator Backend

FastAPI backend for the Mediator app: sentiment analysis, transcript reports API, and SQLite-backed metrics.

## Running the server

From the `backend` directory, start the API server with:

```bash
uv run main.py
```

This uses [uv](https://docs.astral.sh/uv/) to run the project’s Python environment and starts the app with **uvicorn** on **http://0.0.0.0:8000**.

- **Root:** http://localhost:8000/
- **API docs (Swagger):** http://localhost:8000/docs
- **ReDoc:** http://localhost:8000/redoc

## Prerequisites

- **Python 3.12+**
- **uv** (install: `curl -LsSf https://astral.sh/uv/install.sh | sh`)

Install dependencies (if needed):

```bash
uv sync
```

## Environment

Optional `.env` in `backend/` for:

- `ASSEMBLYAI_API_KEY` – used by the audio transcription pipeline
- `OPENROUTER_API_KEY` – used by the analysis pipeline

The reports API reads from a local SQLite DB (`transcripts.db`). Populate it by running the audio processing script (see repo root or `scripts/process_audio.py`).

## Main endpoints

| Endpoint | Description |
|----------|-------------|
| `GET /` | Health check |
| `POST /sentiment` | VADER sentiment for a single text |
| `GET /api/reports/speakers` | List speakers |
| `GET /api/reports/transcripts` | List meeting transcripts |
| `GET /api/reports/categories` | List categories (sentiment, dear_man, fast) |
| `GET /api/reports/metrics` | Aggregated metrics (optional filters) |
| `GET /api/reports/pie-chart-data` | Data for pie charts |

CORS is set for `http://localhost:3000` and `http://127.0.0.1:3000` so the Next.js frontend can call the API.
1 change: 1 addition & 0 deletions backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Backend package
129 changes: 129 additions & 0 deletions backend/analysis_utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""
Analysis utilities for Mediator session analytics.
Provides standalone functions for sentiment analysis and assertiveness scoring.
"""

import re
from typing import Tuple, Dict
import nltk
from nltk.sentiment.vader import SentimentIntensityAnalyzer

# Download VADER lexicon (only needed once)
nltk.download('vader_lexicon', quiet=True)

# Initialize VADER analyzer
sia = SentimentIntensityAnalyzer()


def analyze_sentiment(text: str) -> Tuple[str, float]:
"""
Analyze the sentiment of the given text using VADER.

Args:
text: The text to analyze

Returns:
A tuple of (sentiment_label, sentiment_score) where:
- sentiment_label is one of "positive", "neutral", "negative"
- sentiment_score is the compound score from -1 to 1
"""
# Get VADER scores
scores = sia.polarity_scores(text)

# Find which sentiment (neg, neu, pos) has the highest score
sentiment_scores = {
"negative": scores["neg"],
"neutral": scores["neu"],
"positive": scores["pos"],
}

# Get the label with the highest score
sentiment_label = max(sentiment_scores, key=sentiment_scores.get)

# Use compound score as the sentiment_score (-1 to 1)
sentiment_score = round(scores["compound"], 2)

return sentiment_label, sentiment_score


def calculate_assertiveness(text: str) -> Dict:
"""
Calculate an assertiveness score for DEAR MAN + FAST skill practice.

Measures how directly the user communicated by:
- Counting first-person pronouns ("I", "my", "me")
- Counting declarative statements (sentences without question marks)
- Weighting by total word count

Formula: assertiveness = (first_person_count / word_count * 0.5) + (declarative_ratio * 0.5)

Args:
text: The text to analyze

Returns:
A dict containing:
- assertiveness_score: float 0-1
- first_person_count: int
- declarative_count: int
- total_sentences: int
"""
if not text or not text.strip():
return {
"assertiveness_score": 0.0,
"first_person_count": 0,
"declarative_count": 0,
"total_sentences": 0,
}

# Count first-person pronouns (case-insensitive)
# Match "I", "my", "me" as whole words
first_person_pattern = r'\b(I|my|me)\b'
first_person_matches = re.findall(first_person_pattern, text, re.IGNORECASE)
first_person_count = len(first_person_matches)

# Count words
words = text.split()
word_count = len(words)

# Split into sentences using basic punctuation
# This handles ., !, and ? as sentence terminators
sentences = re.split(r'[.!?]+', text)
# Filter out empty sentences
sentences = [s.strip() for s in sentences if s.strip()]
total_sentences = len(sentences)

# Count declarative statements (sentences that don't end with ?)
# We need to check the original text for question marks
# Find all sentence-ending punctuation and check if they're questions
sentence_endings = re.findall(r'[.!?]+', text)

declarative_count = 0
for ending in sentence_endings:
if '?' not in ending:
declarative_count += 1

# Handle case where text doesn't end with punctuation
if total_sentences > len(sentence_endings):
# The last sentence has no punctuation, assume declarative
declarative_count += (total_sentences - len(sentence_endings))

# Calculate declarative ratio
declarative_ratio = declarative_count / total_sentences if total_sentences > 0 else 0

# Calculate first-person ratio (normalized)
# Cap at 1.0 to prevent scores > 1 when there are many first-person pronouns
first_person_ratio = min(first_person_count / word_count, 1.0) if word_count > 0 else 0

# Calculate assertiveness score using the formula from the brief
# assertiveness = (first_person_count / word_count * 0.5) + (declarative_ratio * 0.5)
assertiveness_score = (first_person_ratio * 0.5) + (declarative_ratio * 0.5)

# Ensure score is between 0 and 1
assertiveness_score = max(0.0, min(1.0, assertiveness_score))

return {
"assertiveness_score": round(assertiveness_score, 2),
"first_person_count": first_person_count,
"declarative_count": declarative_count,
"total_sentences": total_sentences,
}
Binary file added backend/audio_files/dear_man_role_play.mp3
Binary file not shown.
55 changes: 55 additions & 0 deletions backend/audio_transcripts/dear_man_role_play.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{"transcript": [
{
"speaker": "Speaker A",
"message": "Christina. Oh, I had the worst day. Like, you won't believe what my boss did. I can't wait to tell you about this. You will not believe it. It's just horrendous. Like, she just doesn't respect me. She doesn't, like, listen to me. She pushes me too hard. Like, stop."
},
{
"speaker": "Speaker B",
"message": "Stop before you keep going. I see that this is really important to you, and you really want to talk about this right now. And I've had the most overwhelming day today at work, and as much as I wish I had the bandwidth for this right now, I don't. And I really want to support you. I know that this is really important for you to share. I just cannot do this. And I feel that if you can respect me in this decision, if you can respect where I'm coming from, I can be more attentive at a later time."
},
{
"speaker": "Speaker A",
"message": "I mean. I mean, I do get having a hard day. Obviously, I have a hard day, but I had one, too. But, like, I really want to talk. I really feel like I need to talk about it. Like, and you give such good advice, and, like, I feel like you just, like, help me see things clearly. So I just, like, I just really want to talk, you know?"
},
{
"speaker": "Speaker B",
"message": "I know you want to talk. I hear it. I hear how important it is for you, and I just can't do it. You know how it is when we start having these conversations late at night, I then can't go to sleep, and then it just messes up my whole schedule. We can't have the conversation tonight."
},
{
"speaker": "Speaker A",
"message": "So, like, you're shutting me down again."
},
{
"speaker": "Speaker B",
"message": "Kristen, I'm just saying, tonight I can. Is there something else that we can do? Like, is there another way that we can, like, make this work? Like, is there something that you can think of? You know, if I can't do it tonight, can we maybe do it the next day? I mean, what are your thoughts?"
},
{
"speaker": "Speaker A",
"message": "I mean, I guess tomorrow morning will be okay. I guess we could talk about it in the morning."
},
{
"speaker": "Speaker B",
"message": "That would be great."
},
{
"speaker": "Speaker A",
"message": "You have time?"
},
{
"speaker": "Speaker B",
"message": "Yeah. Yeah, I have time. Yeah. We can do it over breakfast."
},
{
"speaker": "Speaker A",
"message": "Okay. Okay. I can do that. I can do that. I mean, I get. I get that you had a hard day. Yeah. That would mean a lot if we could talk about it at breakfast."
},
{
"speaker": "Speaker B",
"message": "Perfect. Trust me, it's going to be better. I'll be better able to listen to you. I'll be more attentive. You'll get your. Your needs met if we do it tomorrow."
},
{
"speaker": "Speaker A",
"message": "Okay. Okay. I can respect that. I can do that. Okay. Thank you. Go back to your book. Now we're good."
}
]
}
13 changes: 13 additions & 0 deletions backend/audio_transcripts/dear_man_role_play.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Speaker A: Christina. Oh, I had the worst day. Like, you won't believe what my boss did. I can't wait to tell you about this. You will not believe it. It's just horrendous. Like, she just doesn't respect me. She doesn't, like, listen to me. She pushes me too hard. Like, stop.
Speaker B: Stop before you keep going. I see that this is really important to you, and you really want to talk about this right now. And I've had the most overwhelming day today at work, and as much as I wish I had the bandwidth for this right now, I don't. And I really want to support you. I know that this is really important for you to share. I just cannot do this. And I feel that if you can respect me in this decision, if you can respect where I'm coming from, I can be more attentive at a later time.
Speaker A: I mean. I mean, I do get having a hard day. Obviously, I have a hard day, but I had one, too. But, like, I really want to talk. I really feel like I need to talk about it. Like, and you give such good advice, and, like, I feel like you just, like, help me see things clearly. So I just, like, I just really want to talk, you know?
Speaker B: I know you want to talk. I hear it. I hear how important it is for you, and I just can't do it. You know how it is when we start having these conversations late at night, I then can't go to sleep, and then it just messes up my whole schedule. We can't have the conversation tonight.
Speaker A: So, like, you're shutting me down again.
Speaker B: Kristen, I'm just saying, tonight I can. Is there something else that we can do? Like, is there another way that we can, like, make this work? Like, is there something that you can think of? You know, if I can't do it tonight, can we maybe do it the next day? I mean, what are your thoughts?
Speaker A: I mean, I guess tomorrow morning will be okay. I guess we could talk about it in the morning.
Speaker B: That would be great.
Speaker A: You have time?
Speaker B: Yeah. Yeah, I have time. Yeah. We can do it over breakfast.
Speaker A: Okay. Okay. I can do that. I can do that. I mean, I get. I get that you had a hard day. Yeah. That would mean a lot if we could talk about it at breakfast.
Speaker B: Perfect. Trust me, it's going to be better. I'll be better able to listen to you. I'll be more attentive. You'll get your. Your needs met if we do it tomorrow.
Speaker A: Okay. Okay. I can respect that. I can do that. Okay. Thank you. Go back to your book. Now we're good.
Loading