Skip to content

Commit

Permalink
Rufina's storyteller machine
Browse files Browse the repository at this point in the history
  • Loading branch information
carlossergiorodrigo committed Sep 22, 2024
0 parents commit 36d5dfe
Show file tree
Hide file tree
Showing 24 changed files with 2,884 additions and 0 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Deploy Backend

on:
push:
branches: [ main ] # adjust this to your main branch name if different

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Build the Docker image
run: docker build -t backend-app ./backend

- name: Run Docker container
env:
ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }}
run: |
docker run -d -p 8000:8000 \
-e ELEVENLABS_API_KEY=$ELEVENLABS_API_KEY \
backend-app
78 changes: 78 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Dependencies
frontend/node_modules
frontend/.pnp
frontend/.pnp.js

# Testing
frontend/coverage

# Next.js
frontend/.next/
frontend/out/

# Production
frontend/build

# Misc
*.DS_Store
*.pem

# Debug
frontend/npm-debug.log*
frontend/yarn-debug.log*
frontend/yarn-error.log*

# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# Vercel
frontend/.vercel

# TypeScript
*.tsbuildinfo

# Python
backend/__pycache__/
backend/*.py[cod]
backend/*$py.class

# Virtual Environment
backend/venv/
backend/env/
backend/ENV/

# PyCharm
backend/.idea/

# VS Code
backend/.vscode/

# Jupyter Notebook
backend/.ipynb_checkpoints

# Docker
docker-compose.override.yml

# Audio files
*.mp3
*.wav
*.ogg

# Logs
*.log

# Database
*.sqlite3

# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
20 changes: 20 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Use an official Python runtime as the base image
FROM python:3.9-slim

# Set the working directory in the container
WORKDIR /app

# Copy the requirements file into the container
COPY requirements.txt .

# Install the Python dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the backend code into the container
COPY . .

# Expose the port that your backend app will run on
EXPOSE 8000

# Command to run the application using uvicorn
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
147 changes: 147 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from langchain_community.llms import ollama
from langchain.prompts import PromptTemplate
from fastapi.middleware.cors import CORSMiddleware
import os
import ffmpeg
from elevenlabs.client import ElevenLabs
from elevenlabs import save
app = FastAPI()

# Initialize the Ollama model
llm = ollama.Ollama(model="mistral-nemo")

# Create a prompt template
story_prompt = PromptTemplate(
input_variables=["topic"],
template=(
"Write a short children's story about {topic}. "
"The story should be engaging and suitable for young children, "
"approximately 5 minutes long when read aloud, and in the language of the topic. "
"Enclose the story within <story> </story> tags. "
"Avoid using special characters like <, >, {{, }}, or *."
"The story should have a happy ending."
"The story should have a moral lesson."
"The story should make me feel good."
"The story should be inspiring."
)
)

# Create the chain using the new recommended approach
story_chain = story_prompt | llm

class StoryRequest(BaseModel):
prompt: str

class AudioRequest(BaseModel):
text: str

@app.post("/api/generate-story")
async def generate_story(request: StoryRequest):
try:
story = story_chain.invoke({"topic": request.prompt})
# Extract the story content from within <story> tags
start_tag = "<story>"
end_tag = "</story>"
start_index = story.find(start_tag)
end_index = story.find(end_tag)

if start_index != -1 and end_index != -1:
extracted_story = story[start_index + len(start_tag):end_index].strip()
else:
extracted_story = story # Fallback to the full story if tags are not found
# Remove <story> and </story> tags if present
extracted_story = extracted_story.replace("<story>", "").replace("</story>", "").strip()
story = extracted_story # Replace the original story with the extracted content

return {"story": story}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

@app.post("/api/generate-audio")
async def generate_audio(request: AudioRequest):
try:
# Get the ElevenLabs API key from environment variable
elevenlabs_api_key = os.environ.get('ELEVENLABS_API_KEY')
if not elevenlabs_api_key:
raise ValueError("ELEVENLABS_API_KEY environment variable is not set")
client = ElevenLabs(api_key=elevenlabs_api_key)

print("Generating audio...")

# Generate speech using ElevenLabs
audio = client.generate(
text=request.text,
voice="Isabela - Spanish Children's Book Narrator",
model="eleven_multilingual_v2"
)

# Save the generated audio
speech_file = "audio/speech.mp3"
save(audio, speech_file)

print("Audio generated")

# Select background music based on the story content
background_music = select_background_music(request.text)

# Combine speech and background music
final_audio_file = mix_audio(speech_file, background_music)

return {"audioUrl": f"/audio/mixed_audio.mp3"}
except Exception as e:
print(f"Error in generate_audio: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))

def select_background_music(story_text):
# For now, we'll just return a default music file
default_music = "audio/default_background.mp3"
if os.path.exists(default_music):
return default_music
else:
print(f"Warning: Default background music file not found at {default_music}")
return None

def mix_audio(speech_file, music_file):
output_file = "audio/mixed_audio.mp3"

if music_file and os.path.exists(music_file):
# Use ffmpeg-python to mix audio
input_speech = ffmpeg.input(speech_file)
input_music = ffmpeg.input(music_file)

# Adjust volume of background music (lowered significantly for subtlety)
lowered_music = ffmpeg.filter(input_music, "volume", volume=0.1 )

# Mix the audio streams
mixed = ffmpeg.filter([input_speech, lowered_music], 'amix', duration='first')

# Output the mixed audio
output = ffmpeg.output(mixed, output_file)
else:
# If no background music, just use the speech file
input_speech = ffmpeg.input(speech_file)
output = ffmpeg.output(input_speech, output_file)

# Run the ffmpeg command
ffmpeg.run(output, overwrite_output=True)

return output_file

# Serve static files (audio)
from fastapi.staticfiles import StaticFiles
app.mount("/audio", StaticFiles(directory="audio"), name="audio")

# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"], # Replace with your frontend URL
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
11 changes: 11 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
fastapi
uvicorn
langchain
langchain-community
pydantic
langchain-ollama
pydub
TTS
ffmpeg-python
elevenlabs
aiofiles
24 changes: 24 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
version: '3.8'

services:
backend:
build: ./backend
ports:
- "8000:8000"
environment:
- ELEVENLABS_API_KEY=${ELEVENLABS_API_KEY}
volumes:
- ./backend:/app
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload

frontend:
build: ./frontend
ports:
- "3001:3001"
volumes:
- ./frontend:/app
- /app/node_modules
command: npm run dev -- -p 3001

volumes:
node_modules:
13 changes: 13 additions & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM node:14

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3001

CMD ["npm", "run", "dev", "--", "-p", "3001"]
3 changes: 3 additions & 0 deletions frontend/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
10 changes: 10 additions & 0 deletions frontend/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import '../styles/globals.css'
import React from 'react'

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className="bg-gray-100">{children}</body>
</html>
)
}
Loading

0 comments on commit 36d5dfe

Please sign in to comment.