Skip to content

Conversation

@Alameen688
Copy link
Collaborator

Summary

Adds an MVP “Podcast” feature with local/S3 storage, a custom player, and RAG‑driven content.

What’s Included

  • Podcast generation
    • Modes: Dialogue (Teacher/Student) and Presentation (Narrator)
    • Voice selection per role (validated against supported voices)
    • Optional focus topics and limiting to selected documents
    • Stores audio as MP3 (local or S3) and saves transcript in DB
    • Delete podcasts (media + DB row)
  • UI
    • Podcast tab: generate form + document filters
    • Custom player: play/pause, ±15s seek, progress, volume, delete
  • Retrieval
    • Pinecone upsert batching to avoid 2MB request limit
    • Pinecone filters by course_id and optional document_ids
    • Structured logs for RAG and podcast flows

How To Use

  1. Go to a course → Podcast tab.
  2. Enter Title (required), choose Mode/Voices.
  3. (Optional) set Focus Topics and select specific Documents.
  4. Generate → Play or Delete.

Config

  • .env
    • PODCAST_STORAGE=local|s3
    • PODCAST_LOCAL_DIR=/app/podcasts
    • S3_BUCKET_NAME, AWS_REGION, credentials
    • PODCAST_TEACHER_VOICE, PODCAST_STUDENT_VOICE

Verification

  • Generate both modes; confirm audio plays and transcript spacing.
  • Limit to selected documents; confirm targeted context.
  • Delete podcast; verify media and DB cleanup.

Screenshot

podcast

Unrelated Podcast Changes (and Why)

These changes are not directly part of the podcast feature but were needed to get the app running on my local machine, including some debugging logs to know the flow of data as I also had issues getting chat to work.

  • 72a94ee — fix prestart errors
    • backend/app/alembic/versions/10368f38610b_fix_delete_document_error.py
      • Removed incorrect chat table re‑creation to prevent DuplicateTable during alembic upgrade.
    • backend/scripts/prestart.sh
      • Ensures migrations + initial data run deterministically on container start.
  • b3b3373 — hold fixes to chat
    • backend/app/api/routes/chat.py, backend/app/services/chat_service.py, backend/app/services/chat_cache.py
      • Added structured logging, cache checks, and safer streaming to make chat more observable and robust.
    • backend/app/services/rag_service.py, backend/app/api/routes/documents.py
      • Fixed Pinecone filtering (by course_id), added diagnostics; later leveraged by podcast RAG.
    • backend/app/schemas/public.py
      • Corrected ChatPublic schema to avoid validation errors when returning chat history.
    • frontend/src/app/layout.tsx
      • Hydration fix to reduce dev error noise: added suppressHydrationWarning on <html> for next-themes. I think it is safe here becuase client toggles the theme class and SSR defaults match, so we’re not hiding real mismatches.
    • frontend/src/components/quiz/quiz-attempts.tsx
      • React key fix to silence list key warning.


@router.get("/{course_id}", response_model=PodcastsPublic)
def list_podcasts(course_id: uuid.UUID, session: SessionDep, current_user: CurrentUser) -> Any:
pods = session.exec(select(Podcast).where(Podcast.course_id == course_id)).all()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some pagination might be useful here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. We can add this as a feature in another PR on both the backend and frontend.

Copy link
Collaborator

@michaelgichia michaelgichia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really impressive, and I will be testing it out in a few hours. I've left a few comments before it is merge.


const baseURL =
process.env.NEXT_PUBLIC_BACKEND_BASE_URL ?? 'http://localhost:8000'
const baseURL = isServer
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are probably running into this error because you are not using the client.

#15 (comment)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove this in this PR but this was an issue I ran into even before implementing podcast. I couldn't log in or sign up. (PS: I'm running the project with docker)

Comment on lines 48 to 50
teacher_voice = body.teacher_voice if body and body.teacher_voice else settings.PODCAST_TEACHER_VOICE
student_voice = body.student_voice if body and body.student_voice else settings.PODCAST_STUDENT_VOICE
narrator_voice = body.narrator_voice if body and body.narrator_voice else settings.PODCAST_TEACHER_VOICE
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these aren't really necessary if we use enum type and specify a default value

Copy link
Collaborator

@deluakin deluakin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good start but some improvement will be needed

Comment on lines -78 to -86
* Total Submitted
*/
total_submitted: number;
/**
* Total Correct
*/
total_correct: number;
/**
* Score Percentage
Copy link
Collaborator Author

@Alameen688 Alameen688 Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: the deletions in the autogenerated files happened automatically after I ran the client generation script #34 (comment)

@Alameen688
Copy link
Collaborator Author

@deluakin @michaelgichia Thanks for your review!
I'm not familiar with the entire stack, but your comments have been helpful. I have updated most of it (if not all) to match the expected structure/convention. Kindly help review again.

Copy link
Collaborator

@michaelgichia michaelgichia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments, and this should be good. Testing now.

"id": embedding_uuid,
"values": embedding,
"metadata": {
"course_id": str(document.course_id),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing this. I was curious why it wasn't working.



@router.get("/course/{course_id}", response_model=PodcastsPublic)
def list_podcasts(course_id: uuid.UUID, session: SessionDep, _current_user: CurrentUser) -> PodcastsPublic:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def list_podcasts(course_id: uuid.UUID, session: SessionDep, _current_user: CurrentUser) -> PodcastsPublic:
def list_podcasts(course_id: uuid.UUID, session: SessionDep, _current_user: CurrentUser, skip: int = 0, limit: int = 50) -> PodcastsPublic:

}

export const config = {
runtime: 'nodejs',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
runtime: 'nodejs',
runtime: 'nodejs',
maxDuration: 300,

console.error('[PodcastAudio] Audio stream error:', error)
}

return Response.json({ error: 'Failed to stream audio' }, { status: 500 })
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return the error like this, otherwise, the client will fail because it expects a specific payload structure:

    const status: number = get(
      error as Record<string, never>,
      'response.status',
      500,
    )
    const body: ErrorResponse = get(
      error as Record<string, never>,
      'response.data.detail',
      {
        detail: 'Internal Server Error',
      },
    )
    return NextResponse.json(body, {status})

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured it didn't matter because I wasn't using the ErrorBox component, which relies on error.detail, but I can add in case of future use

import {CourseWithDocuments} from '@/client'
import FileCard from '@/components/ui/file-card'
import {getCourse} from '@/lib/courses'
import {getCourse} from '@/actions/courses'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for consistency, as I noticed you were importing getCourse directly from '@/actions/courses' in the other parts of the dashboard


import {CourseWithDocuments} from '@/client'
import {getCourse} from '@/lib/courses'
import {getCourse} from '@/actions/courses'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michaelgichia michaelgichia changed the base branch from main to dev October 6, 2025 08:59
@michaelgichia
Copy link
Collaborator

We point the PRs to dev first.

// Configure axios client per request
client.setConfig({
baseURL: process.env.NEXT_PUBLIC_BACKEND_BASE_URL as string,
baseURL: process.env.NEXT_INTERNAL_BACKEND_BASE_URL as string,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michaelgichia After merging to dev, I noticed this was changed to NEXT_PUBLIC_BACKEND_BASE_URL but I had to return it to NEXT_INTERNAL_BACKEND_BASE_URL for things to run on my local.
Not sure if this is fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants