# Project export: FlowState

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: FlowState is a yoga app to promote wellbeing and health. The app uses OpenCV and MediaPipe to detect the accuracy of your yoga pose, and we even have a cute mascot called Shrimpy to cheer you on!
- Devpost: https://devpost.com/software/flowstate-jzxtn0
- GitHub: https://github.com/charred-70/yoga-app
- Video: https://www.youtube.com/embed/DtV5gMnVNug?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Tyffoni (10 commits), ra2y (4 commits), Charlynn Nguyen (3 commits)

## Devpost submission (written by the team)

### Inspiration

As computer science students we are often busy hunched over our computers making our ideas into projects. Its really helpful to take a break and stretch every once in a while, so we create this application to promote yoga and stretching.

### What it does

Users can select which pose they want to perform on our app, and the camera will open up and our awesome and cool mascot will indicate how accurate the user's pose is. The user will also receive helpful advice on how to perform the pose they chose.

### How we built it

We used OpenCV and Mediapipe to capture the camera input and obtain the coordinates of a person in front of the camera. We then created helper functions to help us calcuate the angles between 8 major joints in the body. We used images of people doing the pose to help us find the ideal angles, and we created more helper functions to calculate the accuracy of the pose given the coordinates of a person.

### Challenges we ran into

We had some issues with calling the LLM we chose for our advice. We also had some issues with merge conflicts and communication between frontend and backend, but we're glad we were able to resolve those in the end!

### Accomplishments we're proud of

We are proud of how we were able to connect the dataflow from the backend to an image in the frontend as our first project with computer vision. We were also happy to create actionable health feedback from camera input.

### What we learned

We learned a lot more about how OpenCV and Mediapipe worked, and also more about how to integrate frontend and backend together.

### What's next

We would like to be able to add more yoga poses to our app, and we would also like to be able for users to create a guided yoga routine, and to be able to save the routines.

## README (from the GitHub repository)

This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

Then, run the fastAPI server:

```bash
source /Users/dir-name/yoga-app/venv/bin/activate
python -m uvicorn main:app --host 127.0.0.1 --port 8000
```

It opens on [http://localhost:8000](http://localhost:8000).

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.


## Detected evidence (automated analysis)

Indexed codebase: 21 recognized source files, 64 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- Hugging Face (technology) — detected in the code
- Next.js (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
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (28 of 28)

```
.gitignore
app/globals.css
app/layout.tsx
app/page.tsx
app/routines/layout.tsx
app/routines/page.tsx
app/scoring/layout.tsx
app/scoring/page.tsx
components.json
components/app-sidebar.tsx
components/ui/button.tsx
components/ui/input.tsx
components/ui/separator.tsx
components/ui/sheet.tsx
components/ui/sidebar.tsx
components/ui/skeleton.tsx
components/ui/tooltip.tsx
eslint.config.mjs
hooks/use-mobile.ts
index.html
lib/utils.ts
main.py
next.config.ts
package.json
postcss.config.mjs
README.md
requirements.txt
tsconfig.json
```

### Dependencies

- package.json: @radix-ui/react-dialog@^1.1.15, @radix-ui/react-separator@^1.1.8, @radix-ui/react-slot@^1.2.4, @radix-ui/react-tooltip@^1.2.8, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, class-variance-authority@^0.7.1, clsx@^2.1.1, eslint@^9, eslint-config-next@16.1.3, lucide-react@^0.562.0, next@16.1.3, react@19.2.3, react-dom@19.2.3, tailwind-merge@^3.4.0, tailwindcss@^4, tw-animate-css@^1.4.0, typescript@5.9.3
- requirements.txt: absl-py@==2.3.1, aiohappyeyeballs@==2.6.1, aiohttp@==3.12.15, aiosignal@==1.4.0, annotated-types@==0.7.0, anyio@==4.9.0, asttokens@==3.0.0, attrs@==25.3.0, audioop-lts@==0.2.2, audioread@==3.1.0, beautifulsoup4@==4.14.2, blinker@==1.9.0, cachetools@==6.2.1, certifi@==2025.8.3, cffi@==2.0.0, charset-normalizer@==3.4.4, click@==8.2.1, contourpy@==1.3.3, cycler@==0.12.1, decorator@==5.2.1, diffusers@==0.35.2, distro@==1.9.0, dnspython@==2.7.0, email_validator@==2.2.0, executing@==2.2.1, fastapi, fastapi-cli@==0.0.8, fastapi-cloud-cli, filelock@==3.20.0, Flask@==3.1.1, flatbuffers@==25.12.19, fonttools@==4.60.1, frozenlist@==1.7.0, fsspec@==2025.9.0, google@==3.0.0, google-auth@==2.41.1, google-genai, grpcio@==1.76.0, grpcio-tools@==1.76.0, h11@==0.16.0, hf-xet@==1.1.10, httpcore@==1.0.9, httptools@==0.6.4, httpx@==0.28.1, huggingface-hub@==0.35.3, idna@==3.10, importlib_metadata@==8.7.0, ipython@==9.7.0, ipython_pygments_lexers@==1.1.1, itsdangerous@==2.2.0, jedi@==0.19.2, Jinja2@==3.1.6, jiter@==0.11.1, joblib@==1.5.2, kiwisolver@==1.4.9, lazy_loader@==0.4, librosa@==0.11.0, llvmlite@==0.45.1, markdown-it-py@==3.0.0, MarkupSafe@==3.0.2, matplotlib@==3.10.7, matplotlib-inline@==0.2.1, mdurl@==0.1.2, mediapipe@==0.10.31, mpmath@==1.3.0, msgpack@==1.1.2, multidict@==6.6.4, networkx@==3.5, numba@==0.62.1, numpy@==2.2.6, openai@==2.6.0, opencv-python@==4.12.0.88, packaging@==25.0, parso@==0.8.5, pexpect@==4.9.0, pillow@==12.0.0, platformdirs@==4.5.0, pooch@==1.8.2, prometheus_client@==0.22.1, prompt_toolkit@==3.0.52, propcache@==0.3.2, protobuf@==6.33.0, psutil@==5.9.8, ptyprocess@==0.7.0, pure_eval@==0.2.3, py-grpc-prometheus@==0.7.0, pyasn1@==0.6.1, pyasn1_modules@==0.4.2, pycparser@==2.23, pydantic, pydantic_core@==2.33.2, pydub@==0.25.1, Pygments@==2.19.2, pyparsing@==3.2.5, python-dateutil@==2.9.0.post0, python-dotenv@==1.1.1, python-multipart@==0.0.20, pytube@@ git+https://github.com/joejztang/pytube.git@499268313ada0b971dc5b6718986b27d97731f05, pytubefix@==9.1.1, PyYAML@==6.0.2, regex@==2025.10.23, requests@==2.32.5, rich@==14.1.0, rich-toolkit@==0.14.9, rignore@==0.6.4, rsa@==4.9.1, safetensors@==0.6.2, scikit-learn@==1.7.2, scipy@==1.16.3, sentry-sdk@==2.34.1, setuptools@==80.9.0, shellingham@==1.5.4, six@==1.17.0, sniffio@==1.3.1, sounddevice@==0.5.3, soundfile@==0.13.1, soupsieve@==2.8, soxr@==1.0.0, stack-data@==0.6.3, standard-aifc@==3.13.0, standard-chunk@==3.13.0, standard-sunau@==3.13.0, starlette@==0.27.0, sympy@==1.14.0, tenacity@==9.1.2, threadpoolctl@==3.6.0, tokenizers@==0.22.1, torch@==2.9.0, tqdm@==4.67.1, traitlets@==5.14.3, transformers@==4.57.1, typer@==0.16.0, typing_extensions@==4.14.0, typing-inspection@==0.4.1, tzlocal@==5.3.1, urllib3@==2.5.0, uvicorn@==0.22.0, uvloop@==0.21.0, watchfiles@==1.1.0, wcwidth@==0.2.14, websockets@==15.0.1, Werkzeug@==3.1.3, wheel@==0.45.1, yarl@==1.20.1, zipp@==3.23.0

### Recent commits (newest first)

- display name of pose with video stream
- Merge branch 'master' of github.com:charred-70/yoga-app
- change tab name
- changed pictures
- add text-to-speech automation
- Merge pull request #5 from charred-70/ui-2
- limited accuracy api calls
- Merge branch 'master' into ui-2
- fixed color scheme
- Merge pull request #4 from charred-70/llm-advice
- add gemini text-to-speech
- combined the frontend and backend
- Merge pull request #2 from charred-70/ui-stuff
- Merge branch 'master' into ui-stuff
- hi gang
- Merge pull request #1 from charred-70/render-pose
- add pose accuracy
- to get to the hackathon
- template complete
- why did the cs major cross the road

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

### package.json

```
{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@radix-ui/react-dialog": "^1.1.15",
    "@radix-ui/react-separator": "^1.1.8",
    "@radix-ui/react-slot": "^1.2.4",
    "@radix-ui/react-tooltip": "^1.2.8",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "lucide-react": "^0.562.0",
    "next": "16.1.3",
    "react": "19.2.3",
    "react-dom": "19.2.3",
    "tailwind-merge": "^3.4.0"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.1.3",
    "tailwindcss": "^4",
    "tw-animate-css": "^1.4.0",
    "typescript": "5.9.3"
  }
}

```

### requirements.txt

```
absl-py==2.3.1
aiohappyeyeballs==2.6.1
aiohttp==3.12.15
aiosignal==1.4.0
annotated-types==0.7.0
anyio==4.9.0
asttokens==3.0.0
attrs==25.3.0
audioop-lts==0.2.2
audioread==3.1.0
beautifulsoup4==4.14.2
blinker==1.9.0
cachetools==6.2.1
certifi==2025.8.3
cffi==2.0.0
charset-normalizer==3.4.4
click==8.2.1
contourpy==1.3.3
cycler==0.12.1
decorator==5.2.1
diffusers==0.35.2
distro==1.9.0
dnspython==2.7.0
email_validator==2.2.0
executing==2.2.1
fastapi
fastapi-cli==0.0.8
fastapi-cloud-cli
filelock==3.20.0
Flask==3.1.1
flatbuffers==25.12.19
fonttools==4.60.1
frozenlist==1.7.0
fsspec==2025.9.0
google==3.0.0
google-auth==2.41.1
google-genai
grpcio==1.76.0
grpcio-tools==1.76.0
h11==0.16.0
hf-xet==1.1.10
httpcore==1.0.9
httptools==0.6.4
httpx==0.28.1
huggingface-hub==0.35.3
idna==3.10
importlib_metadata==8.7.0
ipython==9.7.0
ipython_pygments_lexers==1.1.1
itsdangerous==2.2.0
jedi==0.19.2
Jinja2==3.1.6
jiter==0.11.1
joblib==1.5.2
kiwisolver==1.4.9
lazy_loader==0.4
librosa==0.11.0
llvmlite==0.45.1
markdown-it-py==3.0.0
MarkupSafe==3.0.2
matplotlib==3.10.7
matplotlib-inline==0.2.1
mdurl==0.1.2
mediapipe==0.10.31
mpmath==1.3.0
msgpack==1.1.2
multidict==6.6.4
networkx==3.5
numba==0.62.1
numpy==2.2.6
openai==2.6.0
opencv-python==4.12.0.88
packaging==25.0
parso==0.8.5
pexpect==4.9.0
pillow==12.0.0
platformdirs==4.5.0
pooch==1.8.2
prometheus_client==0.22.1
prompt_toolkit==3.0.52
propcache==0.3.2
protobuf==6.33.0
psutil==5.9.8
ptyprocess==0.7.0
pure_eval==0.2.3
py-grpc-prometheus==0.7.0
pyasn1==0.6.1
pyasn1_modules==0.4.2
pycparser==2.23
pydantic
pydantic_core==2.33.2
pydub==0.25.1
Pygments==2.19.2
pyparsing==3.2.5
python-dateutil==2.9.0.post0
python-dotenv==1.1.1
python-multipart==0.0.20
pytube @ git+https://github.com/joejztang/pytube.git@499268313ada0b971dc5b6718986b27d97731f05
pytubefix==9.1.1
PyYAML==6.0.2
regex==2025.10.23
requests==2.32.5
rich==14.1.0
rich-toolkit==0.14.9
rignore==0.6.4
rsa==4.9.1
safetensors==0.6.2
scikit-learn==1.7.2
scipy==1.16.3
sentry-sdk==2.34.1
setuptools==80.9.0
shellingham==1.5.4
six==1.17.0
sniffio==1.3.1
sounddevice==0.5.3
soundfile==0.13.1
soupsieve==2.8
soxr==1.0.0
stack-data==0.6.3
standard-aifc==3.13.0
standard-chunk==3.13.0
standard-sunau==3.13.0
starlette==0.27.0
sympy==1.14.0
tenacity==9.1.2
threadpoolctl==3.6.0
tokenizers==0.22.1
torch==2.9.0
tqdm==4.67.1
traitlets==5.14.3
transformers==4.57.1
typer==0.16.0
typing-inspection==0.4.1
typing_extensions==4.14.0
tzlocal==5.3.1
urllib3==2.5.0
uvicorn==0.22.0
uvloop==0.21.0
watchfiles==1.1.0
wcwidth==0.2.14
websockets==15.0.1
Werkzeug==3.1.3
wheel==0.45.1
yarl==1.20.1
zipp==3.23.0

```

### main.py

```python
from math import acos, sqrt
from urllib import response
from fastapi import FastAPI
from fastapi.concurrency import asynccontextmanager
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import cv2
import mediapipe as mp
import time
from fastapi.responses import StreamingResponse
import threading
from google import genai
import os
from dotenv import load_dotenv

load_dotenv()
client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))



app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:3000",
        "http://127.0.0.1:3000",
        "http://localhost:3001",
        "http://127.0.0.1:3001",
        "*"
    ],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

cap = None
streaming = False
cap_lock = threading.Lock()
curr_pose = None
targetAngles = [[165, 20, 20, 175, 175, 175, 175, 175],[160, 85, 85, 160, 150, 150, 175, 175],[165, 165, 165, 165, 70, 70, 175, 175],[130, 40, 30, 130, 70, 70, 110, 110], [35, 40, 30, 35, 170, 110, 180, 35]]
poseTolerance = [10, 20, 20, 20, 15]
poseList = ['mountain', 'seal', 'downwardDog','frog','tree']
acc = 0

class PoseRequest(BaseModel):
    pose: str


class PoseDetector():
    def __init__(self, staticImageMode=False, modelComplexity=1, smoothLandmarks=True, enableSegmentation=False, smoothSegmentation=True, minDetectionConfidence=0.5, minTrackingConfidence=0.5):
        self.staticImageMode = staticImageMode
        self.modelComplexity = modelComplexity
        self.smoothLandmarks = smoothLandmarks
        self.enableSegmentation = enableSegmentation
        self.smoothSegmentation = smoothSegmentation
        self.minDetectionConfidence = minDetectionConfidence
        self.minTrackingConfidence = minTrackingConfidence

        self.mpDraw = mp.solutions.drawing_utils
        self.mpPose = mp.solutions.pose
        self.pose = self.mpPose.Pose(self.staticImageMode, self.modelComplexity, self.smoothLandmarks, self.enableSegmentation, self.smoothSegmentation, self.minDetectionConfidence, self.minTrackingConfidence)

    # dis method draws on video
    def findPose(self, img, draw=True):

        imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        self.results = self.pose.process(imgRGB)

        if self.results.pose_landmarks:
            if draw:
                self.mpDraw.draw_landmarks(img, self.results.pose_landmarks, self.mpPose.POSE_CONNECTIONS)

        return img

    def findPosition(self, img, draw=False):
        lmList = []
        if not self.results.pose_landmarks:
            return lmList

        for id, lm in enumerate(self.results.pose_landmarks.landmark):
            h, w, c = img.shape
            cx, cy = int(lm.x * w), int(lm.y * h)

            lmList.append([id, cx, cy])

            if draw:
                cv2.circle(img, (cx, cy), 7, (255, 0, 0), cv2.FILLED)

        return lmList

def calculate_angle(x1, y1, x2, y2, x3, y3):
    v1 = (x1 - x2, y1 - y2)
    v2 = (x3 - x2, y3 - y2)
    dot = v1[0] * v2[0] + v1[1] * v2[1]
    v1_len = sqrt(v1[0]**2 + v1[1]**2)
    v2_len = sqrt(v2[0]**2 + v2[1]**2)
    angle = acos(dot / (v1_len * v2_len)) * (180.0 / 3.14159265)
    return angle

def angle_list(lmlist):
     angleList = []
    #user's right elbow (node 16, 14, 12)
     right_elbow = calculate_angle(lmlist[16][1], lmlist[16][2], lmlist[14][1], lmlist[14][2], lmlist[12][1], lmlist[12][2])
     angleList.append(int(right_elbow))
     #user's right armpit (node 14, 12, 24)
     right_armpit = calculate_angle(lmlist[14][1], lmlist[14][2], lmlist[12][1], lmlist[12][2], lmlist[24][1], lmlist[24][2])
     angleList.append(int(right_armpit))
    #user's left armpit (node 13, 11, 23)
     left_armpit = calculate_angle(lmlist[13][1], lmlist[13][2], lmlist[11][1], lmlist[11][2], lmlist[23][1], lmlist[23][2])
     angleList.append(int(left_armpit))
    #user's left elbow (node 15, 13, 11)
     left_elbow = calculate_angle(lmlist[15][1], lmlist[15][2], lmlist[13][1], lmlist[13][2], lmlist[11][1], lmlist[11][2])
     angleList.append(int(left_elbow))
    #user's right hip (node 24, 12, 26)
     right_hip = calculate_angle(lmlist[12][1], lmlist[12][2], lmlist[24][1], lmlist[24][2], lmlist[26][1], lmlist[26][2])
     angleList.append(int(right_hip))
    #user's left hip (node 23, 11, 25)
     left_hip = calculate_angle(lmlist[11][1], lmlist[11][2], lmlist[23][1], lmlist[23][2], lmlist[25][1], lmlist[25][2])
     angleList.append(int(left_hip))
    #user's right knee (node 26, 24, 28)
     right_knee = calculate_angle(lmlist[24][1], lmlist[24][2], lmlist[26][1], lmlist[26][2], lmlist[28][1], lmlist[28][2])
     angleList.append(int(right_knee))
    #user's left knee (node 25, 23, 27)
     left_knee = calculate_angle(lmlist[23][1], lmlist[23][2], lmlist[25][1], lmlist[25][2], lmlist[27][1], lmlist[27][2])
     angleList.append(int(left_knee))
     return angleList

# returns the accuracy of the user's pose to the chosen pose
def check_angles(angleList):
    global curr_pose, targetAngles, poseList, poseTolerance
    ind = poseList.index(curr_pose)
    target = targetAngles[ind]
    tolerance = poseTolerance[ind]
    total_error = 0
    for i in range(len(angleList)):
        total_error += abs(angleList[i] - target[i])
    average_error = total_error / len(angleList)
    if average_error > tolerance:
        return 0
    elif average_error > tolerance / 2:
        return 1
    return 2

def gen_frames():
    global cap, streaming, acc
    # cap = cv2.VideoCapture(0, cv2.CAP_AVFOUNDATION)

    pTime = 0
    detector = PoseDetector()
    try:
        while True:
            if not streaming:
                continue
            if cap is None:
                time.sleep(1)
                print("Camera not initialized")
                continue
            success, frame = cap.read()
            if not success:
                print("Failed to grab frame")
                break
            else:
                try:
                
[truncated — 2707 more characters]
```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "FlowState",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}

```

### app/page.tsx

```typescript
"use client";
import Link from "next/link";
import React from "react";
import Image from "next/image";

export default function Home() {
  return (
    <div className="flex flex-col min-h-screen bg-linear-to-br from-red-50 via-pink-50 to-purple-50 font-sans">
      <nav className="w-full bg-white/80 backdrop-blur-md shadow-sm fixed top-0 left-0 right-0 z-50 border-b border-pink-100">
        <div className="max-w-7xl mx-auto px-6 py-4">
          <div className="flex justify-between items-center">
            <div className="text-2xl font-bold bg-linear-to-r from-pink-300 to-red-400 bg-clip-text text-transparent">
              FlowState
            </div>
            <ul className="flex gap-8 list-none">
              <li>
                <a
                  href="#home"
                  className="text-gray-700 hover:text-pink-500 transition-all duration-300 font-medium relative after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-0 after:bg-pink-500 after:transition-all after:duration-300 hover:after:w-full"
                >
                  Home
                </a>
              </li>
              <li>
                <a
                  href="#contact"
                  className="text-gray-700 hover:text-pink-500 transition-all duration-300 font-medium relative after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-0 after:bg-pink-500 after:transition-all after:duration-300 hover:after:w-full"
                >
                  Contact
                </a>
              </li>
            </ul>
          </div>
        </div>
      </nav>

      <main className="flex-1 flex items-center justify-center pt-24 pb-12 px-6">
        <div className="flex flex-col lg:flex-row lg:items-center gap-12 lg:gap-20 max-w-7xl w-full">
          <div className="flex flex-col lg:w-1/2 space-y-6">
            <h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold text-gray-900 leading-tight">
              Where mindful movement meets
              <span className="block bg-linear-to-r from-red-300 to-pink-400 bg-clip-text text-transparent mt-2">
                intelligent feedback
              </span>
            </h1>

            <p className="text-xl text-gray-600 leading-relaxed max-w-xl">
              Turn every yoga pose into insight and improve your health through
              precise, personalized alignment.
            </p>

            <div className="flex gap-4 pt-4">
              <Link href="/routines">
                <button className="px-8 py-4 bg-red-400 text-white rounded-full font-semibold hover:shadow-2xl hover:scale-105 transition-all duration-300 shadow-lg">
                  Get Started
                </button>
              </Link>
              <button className="px-8 py-4 bg-white text-gray-700 rounded-full font-semibold hover:shadow-lg transition-all duration-300 border-2 border-gray-200 hover:border-pink-300">
                Learn More
              </button>
            </div>
          </div>

          <div className="lg:w-1/2 flex justify-center lg:justify-end relative">
            <div className="relative">
              <div className="absolute -top-10 -left-10 w-72 h-72 bg-pink-300 rounded-full mix-blend-multiply filter blur-3xl opacity-30 animate-pulse"></div>
              <div className="absolute -bottom-10 -right-10 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-3xl opacity-30 animate-pulse animation-delay-2000"></div>

              <div className="relative bg-white-500/10 p-8 rounded-3xl hover:shadow-pink-200 transition-shadow duration-500">
                <Image
                  src="/neutral.png"
                  width={500}
                  height={500}
                  alt="test"
                  className="relative z-10"
                />
              </div>
            </div>
          </div>
        </div>
      </main>

      <footer className="w-full py-8 text-center bg-white/50 backdrop-blur-sm border-t border-pink-100">
        <p className="text-gray-600"> </p>
      </footer>
    </div>
  );
}

```

### app/scoring/layout.tsx

```typescript
import React from 'react';

export default function scoringLayout({
    children,
}: {
    children: React.ReactNode;
}) {
    return (
        <section>

            {children}
        </section>
    );
}
```

### app/routines/layout.tsx

```typescript
import { SidebarProvider } from "@/components/ui/sidebar"
import { AppSidebar } from "@/components/app-sidebar"

export default function Layout({ children }: { children: React.ReactNode }) {
    return (
        <SidebarProvider>
            <div className="flex min-h-screen w-full">
                <AppSidebar />
                <main className="flex-1 w-full">

                    {children}
                </main>
            </div>
        </SidebarProvider>
    )
}
```

### app/scoring/page.tsx

```typescript
"use client";
import Link from "next/link";
import React, { use, useEffect, useState } from "react";
import Image from "next/image";
import shrimpy_happy from "../../public/shrimpHappy.png";
import shrimpy_sad from "../../public/shrimpSad.png";
import shrimpy_neutral from "../../public/neutral.png";
let intervalId: string | number | NodeJS.Timeout | null | undefined;

export default function ScoringPage() {
  // use state variable to set shrimpy image
  const [imgSrc, setShrimpy] = useState(shrimpy_neutral);
  const [timeEnd, setTimeEnd] = useState(false);

  // function to repeatedly fetch accuracy score
  // and updates the shrimpy image
  async function get_accuracy() {
    console.log("get accuracy called");
    try {
      const res = await fetch(`http://127.0.0.1:8000/api/accuracy-score`, {
        method: "GET",
      });
      const data = await res.json();
      const accuracy = data.value;
      if (accuracy == 2) {
        setShrimpy(shrimpy_happy);
      } else if (accuracy == 1) {
        setShrimpy(shrimpy_neutral);
      } else if (accuracy == 0) {
        setShrimpy(shrimpy_sad);
      }
    } catch (err) {
      console.error(err);
    }
  }

  async function get_advice() {
    try {
      const r = await fetch("http://127.0.0.1:8000/api/advice", {
        method: "GET",
      });
      const adviceData = await r.json();
      console.log(adviceData);
      speakAdvice(adviceData.advice);
    } catch (err) {
      console.error(err);
    }
  }
  async function speakAdvice(advice: string) {
    if (typeof window !== "undefined" && window.speechSynthesis) {
      const utterance = new SpeechSynthesisUtterance(advice);
      utterance.rate = 0.7;
      window.speechSynthesis.speak(utterance);
    } else {
      alert("text to speech is not supported for some reason idk man..");
    }
  }

  useEffect(() => {
    const timer = setTimeout(() => {
      setTimeEnd(true);
      stop();
    }, 60000); // 1 minute timer
  });

  useEffect(() => {
    const pose = async () => {
      try {
        const res = await fetch(`http://127.0.0.1:8000/api/curr-pose`, {
          method: "GET",
        });
        const data = await res.json();
        console.log(data);
        var pose = data.pose;
        if (pose == "mountain") {
          pose = "Mountain Pose";
        } else if (pose == "seal") {
          pose = "Seal Pose";
        } else if (pose == "downwardDog") {
          pose = "Downward Dog Pose";
        } else if (pose == "frog") {
          pose = "Frog Pose";
        } else if (pose == "tree") {
          pose = "Tree Pose";
        }
        const poseElement = document.getElementById("current-pose");
        if (poseElement) {
          poseElement.textContent = pose;
        }
      } catch (err) {
        console.error(err);
      }
    };
    pose();
  });

  // starts the interval that constantly calls get_accuracy
  // every 2 secs
  useEffect(() => {
    const intervalId = setInterval(get_accuracy, 1000);
    return () => clearInterval(intervalId);
  }, []);

  useEffect(() => {
    const adviceId = setTimeout(get_advice, 8000);
    // console.log("im tryna speak");
    return () => clearTimeout(adviceId);
  }, []);

  async function stop() {
    await fetch(`http://127.0.0.1:8000/api/stop-stream`, { method: "POST" });
  }
  return (
    <>
      <Link href="/routines">
        <button
          className="px-8 py-4 bg-red-400
                  text-white rounded-full font-semibold
                  hover:bg-pink-400 transition-colors shadow-lg
                  fixed top-4 left-4"
          onClick={() => {
            stop();
          }}
        >
          Back
        </button>
      </Link>
      <div className="flex flex-row min-h-screen items-center justify-center bg-linear-to-br from-sky-50 via-pink-50 to-purple-50 font-sans dark:bg-zinc-100">
        <div className="flex flex-col size-full max-w-7xl mx-auto bg-red-300 p-8 rounded-lg shadow-lg mb-15 mt-20">
          <div className="relative w-full h-130 rounded overflow-hidden bg-white">
            <img
              src="http://127.0.0.1:8000/api/video-feed"
              alt="test"
              className="video feed"
            />
          </div>
        </div>
        <div className="flex flex-col relative bg-white-500/10 p-7 rounded-3xl hover:shadow-pink-200 transition-shadow duration-500">
          <h1
            className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold text-red-400 leading-tight mb-4"
            id="current-pose"
          >
            Pose
          </h1>
          <h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold text-gray-900 leading-tight">
            Accuracy:
          </h1>
          <Image
            src={imgSrc}
            alt="shrimpy image here"
            width={500}
            height={500}
            className="relative z-10"
          />
        </div>
        {timeEnd && (
          <p className="text-3xl font-bold text-red-600 ml-8">Time's Up!</p>
        )}
      </div>
    </>
  );
}

```

### app/routines/page.tsx

```typescript
"use client";
import Link from "next/link";
import React from "react";
import Image from "next/image";
import { useState, useEffect } from "react";
import { Check } from "lucide-react";

let intervalId: string | number | NodeJS.Timeout | null | undefined;

export default function RoutinePage() {
  const [selectedPoses, setSelectedPoses] = useState<number[]>([]);

  const poses = [
    { id: 1, name: "Mountain Pose", image: "/mountain.jpg", value: "mountain" },
    { id: 2, name: "Seal Pose", image: "/seal.jpg", value: "seal" },
    {
      id: 3,
      name: "Downward Dog",
      image: "/downward.jpeg",
      value: "downwardDog",
    },
    { id: 4, name: "Frog Pose", image: "/frog.jpg", value: "frog" },
    { id: 5, name: "Tree Pose", image: "/tree.jpg", value: "tree" },
  ];

  const togglePose = (poseId: number) => {
    setSelectedPoses((prev) =>
      prev.includes(poseId)
        ? prev.filter((id) => id !== poseId)
        : [...prev, poseId],
    );
  };

  async function sendPose() {
    const input = selectedPoses[0];
    if (!input) return;

    const pose = poses[input - 1].value;
    if (input == null) return;

    await fetch("http://127.0.0.1:8000/api/pose-checker", {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ pose }),
    });
    await fetch("http://127.0.0.1:8000/api/start-stream", { method: "POST" });
  }

  return (
    <div className="flex flex-col min-h-screen bg-linear-to-br from-pink-50 via-purple-50 to-sky-50 font-sans">
      <div className="w-full pt-16 pb-12 px-6">
        <div className="max-w-7xl mx-auto text-center space-y-4">
          <h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold bg-linear-to-r from-pink-400 to-red-200 bg-clip-text text-transparent">
            Create your Routine!
          </h1>

          <p className="text-lg text-gray-600 max-w-2xl mx-auto">
            Select your favorite poses to create a personalized yoga routine.
          </p>
        </div>
      </div>

      {/* Poses Grid */}
      <div className="flex-1 px-6 pb-32">
        <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 max-w-7xl mx-auto">
          {poses.map((pose) => (
            <div
              key={pose.id}
              onClick={() => togglePose(pose.id)}
              className={`group relative cursor-pointer rounded-3xl transition-all duration-300 transform hover:scale-105 ${selectedPoses.includes(pose.id)
                ? "bg-linear-to-br from-red-300 to-pink-400 shadow-2xl shadow-pink-300"
                : "bg-white hover:shadow-xl shadow-md"
                }`}
            >
              {/* Selection Indicator */}
              {selectedPoses.includes(pose.id) && (
                <div className="absolute -top-3 -right-3 bg-white rounded-full p-3 shadow-lg z-10 animate-bounce">
                  <Check size={24} className="text-red-300" />
                </div>
              )}

              {/* Card Content */}
              <div className="p-6 space-y-4">
                {/* Image Container */}
                <div
                  className={`relative aspect-square w-full rounded-2xl overflow-hidden transition-all duration-300 ${selectedPoses.includes(pose.id)
                    ? "bg-white ring-4 ring-white/50"
                    : "bg-gray-50 group-hover:bg-pink-50"
                    }`}
                >
                  <Image
                    src={pose.image}
                    alt={pose.name}
                    fill
                    className="object-contain p-4 transition-transform duration-300 group-hover:scale-110"
                  />
                </div>

                {/* Pose Name */}
                <p
                  className={`text-xl font-semibold text-center transition-colors ${selectedPoses.includes(pose.id)
                    ? "text-white"
                    : "text-gray-800 group-hover:text-pink-600"
                    }`}
                >
                  {pose.name}
                </p>
              </div>

              {/* Subtle gradient overlay on hover */}
              {!selectedPoses.includes(pose.id) && (
                <div className="absolute inset-0 rounded-3xl bg-linear-to-br from-pink-400/0 to-purple-500/0 group-hover:from-pink-400/5 group-hover:to-purple-500/5 transition-all duration-300" />
              )}
            </div>
          ))}
        </div>
      </div>

      {/* Floating Action Button */}
      {selectedPoses.length > 0 && (
        <div className="fixed bottom-8 left-0 right-0 flex justify-center px-6 z-50">
          <Link href="/scoring">
            <button
              onClick={sendPose}
              className="group bg-linear-to-r from-red-300 to-pink-400 hover:from-red-500 hover:to-pink-500 text-white font-bold py-5 px-10 rounded-full shadow-2xl shadow-pink-300 transform transition-all duration-300 hover:scale-110 flex items-center gap-3 text-lg backdrop-blur-sm"
            >
              <div className="bg-white/20 rounded-full p-2">
                <Check
                  size={24}
                  className="group-hover:rotate-12 transition-transform"
                />
              </div>
              <span>
                Begin with {selectedPoses.length}{" "}
                {selectedPoses.length === 1 ? "Pose" : "Poses"}
              </span>
              <div className="bg-white/20 rounded-full px-3 py-1 text-sm font-bold">
                {selectedPoses.length}
              </div>
            </button>
          </Link>
        </div>
      )}
    </div>
  );
}

```

### next.config.ts

```typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
};

export default nextConfig;

```

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