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
18 changes: 16 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
node_modules
dist
.env
backend/.env
backend/firebase-service-account.json
# dependencies
/node_modules
/.pnp
.pnp.js
backend/.venv/
__pycache__/
*.py[cod]


# local audio recordings and generated voice responses
*.mp3
*.m4a
*.wav
*.webm
*.ogg
/tmp_pdf_pages_v2

# testing
/coverage

Expand All @@ -23,4 +37,4 @@ exampleGuide
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock
*.log
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# DevDay 2026 — Frontend Starter

Welcome to the HackUTD DevDay 2026 frontend workshop.

During this workshop, we will turn a simple React recipe browser into an AI-powered cooking assistant.

## Requirements

Before starting, make sure you have:

- Git
- Node.js 20.19 or newer
- VS Code or another code editor

## Clone the starter branch

```bash
git clone -b frontendStarter https://github.com/hackutd/DevDay-2026.git
cd DevDay-2026
4 changes: 4 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
GEMINI_API_KEY=your_gemini_api_key_here
ELEVENLABS_API_KEY=your_elevenlabs_api_key_here
ELEVENLABS_VOICE_ID=your_elevenlabs_voice_id_here
FIREBASE_SERVICE_ACCOUNT_PATH=./firebase-service-account.json
1 change: 1 addition & 0 deletions backend/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.11
46 changes: 46 additions & 0 deletions backend/firebase_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import os
import firebase_admin
from firebase_admin import credentials, firestore
from fastapi import HTTPException

# make sure Firebase is initialized before accessing the database
def get_firestore_db():
service_account_path = os.getenv("FIREBASE_SERVICE_ACCOUNT_PATH")
if not service_account_path:
raise HTTPException(status_code=500, detail="Missing Firebase service account path")

if not firebase_admin._apps:
cred = credentials.Certificate(service_account_path)
firebase_admin.initialize_app(cred)

return firestore.client()


def get_favorites(user_id: str) -> list:
db = get_firestore_db()
favorite_doc = db.collection("favorites").document(user_id).get()

if not favorite_doc.exists:
return []

favorite_data = favorite_doc.to_dict() or {}
return favorite_data.get("items", [])


# code for toggling user favorites:
def toggle_favorite(user_id: str, food: dict) -> list:
db = get_firestore_db()
favorite_ref = db.collection("favorites").document(user_id)
favorites = get_favorites(user_id)

# Code here to add the food to favorites or remove it if already favorited
food_id = food.get("id")
already_favorited = any(item.get("id") == food_id for item in favorites)

if already_favorited:
favorites = [item for item in favorites if item.get("id") != food_id]
else:
favorites.append(food)

favorite_ref.set({"items": favorites})
return favorites
191 changes: 191 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# import some important libraries
import os

import requests
from dotenv import load_dotenv
from fastapi import FastAPI, File, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import Response
from firebase_service import get_favorites, toggle_favorite
from google import genai
from pydantic import BaseModel

load_dotenv()

# define some of our constants
GEMINI_MODEL = "gemini-3.5-flash-lite"
ELEVENLABS_MODEL = "eleven_multilingual_v2"
ELEVENLABS_STT_MODEL = "scribe_v2"

# create our fast api app
app = FastAPI(title="Chef Voice Backend") # this line wont be here in hacker version

# need this so our frontend and backend can talk to each other
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)


# code health endpoint here:
# to test if our backend is running
@app.get("/health")
def health():
return {"status": "ok"}


# helper function to generate a chef-like reply using Gemini API
def make_chef_reply(user_text: str, gemini_api_key: str) -> str:
prompt = (
"You are a distinguished chef with a quirky sense of humor. Listen for a user's cooking question or request. "
"Always be very concise: answer in 1 to 2 short sentences max. "
"Focus on practical cooking help. "
"Use simple words, light humor, and occasional clever rmarks "
"Do not use markdown, lists, or long explanations. "
f"User: {user_text}"
)

# code gemini api call here:
try:
client = genai.Client(api_key=gemini_api_key)
gemini_response = client.models.generate_content(
model=GEMINI_MODEL,
contents=prompt,
)
reply_text = gemini_response.text.strip()
except Exception as error:
raise HTTPException(status_code=502, detail=f"Gemini request failed: {error}")

if not reply_text:
raise HTTPException(status_code=502, detail="Gemini returned no text")

return reply_text

# function to make audio from the chef reply using ElevenLabs API
def make_audio(reply_text: str, elevenlabs_api_key: str, voice_id: str) -> bytes:
# code elevenlabs text-to-speech API call here:
elevenlabs_response = requests.post(
f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}",
headers={
"xi-api-key": elevenlabs_api_key,
"Content-Type": "application/json",
"Accept": "audio/mpeg",
},
json={
"text": reply_text,
"model_id": ELEVENLABS_MODEL,
},
timeout=300,
)
# end of elevenlabs text-to-speech API call

# if there is an erorr we will return a 502 error to the frontend
if elevenlabs_response.status_code != 200:
raise HTTPException(
status_code=502,
detail=f"ElevenLabs audio generation failed: {elevenlabs_response.text}",
)

return elevenlabs_response.content


# function to transcribe audio using ElevenLabs API

def transcribe_audio(file: UploadFile, elevenlabs_api_key: str) -> str:
audio_bytes = file.file.read() # file is an object which has a file attribute, read bytes with .read()
if not audio_bytes:
raise HTTPException(status_code=400, detail="Uploaded audio file is empty")

# code elevenlabs speech-to-text API call here:
stt_response = requests.post(
"https://api.elevenlabs.io/v1/speech-to-text",
headers={"xi-api-key": elevenlabs_api_key},
files={
"file": (
file.filename or "recording.webm",
audio_bytes,
file.content_type or "audio/webm",
)
},
data={"model_id": ELEVENLABS_STT_MODEL},
timeout=300,
)
# end of elevenlabs speech-to-text API call

# check if the transcription was successful
if stt_response.status_code != 200:
raise HTTPException(
status_code=502,
detail=f"ElevenLabs transcription failed: {stt_response.text}",
)

# get transcript from the response
transcript = stt_response.json().get("text", "").strip()
if not transcript:
raise HTTPException(status_code=502, detail="ElevenLabs returned no transcript")

return transcript


# to generate a chef-like voice from an audio file
@app.post("/chef/voice/audio")
def create_chef_voice_from_audio(file: UploadFile = File(...)):
# code here
gemini_api_key = os.getenv("GEMINI_API_KEY")
elevenlabs_api_key = os.getenv("ELEVENLABS_API_KEY")
voice_id = os.getenv("ELEVENLABS_VOICE_ID")

# code here : if missing some of our API keys, return an error
if not gemini_api_key:
raise HTTPException(status_code=500, detail="Missing Gemini API key")
if not elevenlabs_api_key or not voice_id:
raise HTTPException(status_code=500, detail="Missing ElevenLabs API key or voice ID")

# code here
# call our helper functions
transcript = transcribe_audio(file, elevenlabs_api_key)
reply_text = make_chef_reply(transcript, gemini_api_key)
audio_bytes = make_audio(reply_text, elevenlabs_api_key, voice_id)

# return our audio
return Response(
content=audio_bytes,
media_type="audio/mpeg",
)


#add request models here:
# make a model for food items for the favorites endpoint:
class FoodItem(BaseModel):
id: int
name: str
emoji: str
description: str
cookTime: int
difficulty: str
ingredients: list[str]
instructions: list[str]

# make a model for favorite requests for the favorites endpoint:
class FavoriteRequest(BaseModel):
userId: str
food: FoodItem

#code get favorites endpoint here:
@app.get("/favorites/{user_id}")
def get_user_favorites(user_id: str):
return {"favorites": get_favorites(user_id)}


# make endpoint for toggling user favorites
@app.post("/favorites/toggle")
def toggle_user_favorite(request: FavoriteRequest):
updated_favorites = toggle_favorite(
request.userId,
request.food.model_dump(exclude_none=True),
)

return {"favorites": updated_favorites}
8 changes: 8 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
fastapi==0.115.6
uvicorn==0.34.0
pydantic==2.12.5
python-dotenv==1.0.1
google-genai==2.13.0
requests==2.32.3
python-multipart==0.0.20
firebase-admin==7.5.0
Loading