The AI Personal Trainer is a "voice-first" workout application designed to be an interactive partner in your fitness journey. Unlike traditional workout trackers, this app allows you to manage your entire workout—from creating a personalized plan to logging sets and getting real-time feedback—using only your voice. This keeps you focused on your exercises, not on your phone.
- AI-Powered Onboarding: Get a personalized workout program generated by AI after a brief, conversational onboarding session.
- Voice-Controlled Workouts: Log sets, ask for exercise technique reminders, and adjust your workout on the fly with natural voice commands.
- Real-time Audio Processing: Utilizes a dual-model Gemini architecture for responsive, low-latency dialogue and highly accurate data capture.
- Detailed Progress Analytics: Track your progress with dynamic charts for strength, volume, and personal records for each exercise.
- Automatic Calendar Integration: Seamlessly schedule your AI-generated workouts into your Google Calendar.
- Proactive Plan Adaptation: The AI analyzes your performance and suggests intelligent adjustments to your program to ensure continuous progress.
The core of this application is its voice-first interaction model. The primary way to interact with the app, especially during a workout, is through voice. The visual interface acts as a secondary support system, displaying information and providing alternative touch controls. This design minimizes distractions and allows you to stay focused on your training.
To achieve a seamless voice experience, the application uses a sophisticated dual-model architecture:
- Dialogue Model (
gemini-2.5-flash-preview-native-audio-dialog): This model is optimized for low-latency, real-time streaming dialogue. It handles the immediate back-and-forth conversation, including live transcription and text-to-speech, making the interaction feel natural and responsive. - Analysis Model (
Gemini 2.5 Flash): After a user speaks a command (e.g., "I did 10 reps with 60 kilos"), the full transcript is sent to a more powerful text-based model. This model's sole job is to accurately extract structured JSON data from the user's natural language (e.g.,{ "reps": 10, "weight": 60 }).
This hybrid approach provides the best of both worlds: the speed of a live dialogue model and the analytical precision of a larger model.
- Frontend: React, TypeScript, Vite
- AI & Backend: Google Gemini (Live Audio & Pro models), Firebase (Firestore, Authentication)
- Data Visualization: Recharts
- Testing: Vitest, React Testing Library
Follow these steps to set up and run the project locally.
- Node.js (v18 or later recommended)
npmor a compatible package manager
git clone https://github.com/your-username/ai-personal-trainer.git
cd ai-personal-trainerThis project uses Firebase for authentication and its Firestore database.
-
Go to the Firebase Console and create a new project.
-
In your project settings, find your web app's Firebase configuration object. It will look something like this:
const firebaseConfig = { apiKey: "YOUR_API_KEY", authDomain: "YOUR_PROJECT_ID.firebaseapp.com", projectId: "YOUR_PROJECT_ID", storageBucket: "YOUR_PROJECT_ID.appspot.com", messagingSenderId: "YOUR_SENDER_ID", appId: "YOUR_APP_ID" };
-
Open the file
src/services/firebaseConfig.tsand replace the placeholder configuration with your own project's configuration.
You need a Google Gemini API key to use the AI features.
-
Create a new file named
.envin the root of the project. -
Add your Gemini API key to the file:
GEMINI_API_KEY=your_gemini_api_key_here
-
Install the project dependencies:
npm install
-
Run the development server:
npm run dev
The application should now be running on
http://localhost:5173(or another port if 5173 is in use).
npm run dev: Starts the development server with Hot Module Replacement (HMR).npm run build: Builds the application for production.npm run preview: Serves the production build locally for previewing.npm run test: Runs the automated tests using Vitest and opens the interactive UI.
This project includes a set of robust Firebase Security Rules to protect user data. The core principle is that users have full control over their own data but cannot read or modify the data of other users.
- User Profiles: Users can only manage their own profile information.
- Exercise Library: The global exercise library is read-only for all users to ensure data consistency.
- User-Generated Content: Programs, workouts, and session logs can only be created, read, updated, and deleted by their original creator.
To deploy these rules to your Firebase project:
- Navigate to your project in the Firebase Console.
- Go to Firestore Database > Rules tab.
- Copy the rules below and paste them into the editor, replacing any existing rules.
- Click Publish.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// A user can only read and write to their own document.
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
}
// The exercises collection is a global library, readable by any authenticated user,
// but not writable from the client.
match /exercises/{exerciseId} {
allow read: if request.auth != null;
allow write: if false;
}
// Programs can be created by any authenticated user, but only the creator
// can read, update, or delete their own programs. The same logic applies
// to the nested workouts.
match /programs/{programId} {
allow read, update, delete: if resource.data.createdBy == request.auth.uid;
allow create: if request.resource.data.createdBy == request.auth.uid;
// A user can read/write workouts only if they own the parent program.
match /workouts/{workoutId} {
// Read, update, delete require checking the parent program document.
allow read, update, delete: if get(/databases/$(database)/documents/programs/$(programId)).data.createdBy == request.auth.uid;
// The 'create' rule needs to handle two cases:
// 1. Initial program creation (in a batch), where the parent program doesn't exist yet.
// 2. Updating an existing program (also a batch), where the parent program does exist.
allow create: if (
// Case 2: Parent exists, check ownership. This secures updates.
exists(/databases/$(database)/documents/programs/$(programId)) &&
get(/databases/$(database)/documents/programs/$(programId)).data.createdBy == request.auth.uid
) || (
// Case 1: Parent does not exist. This only happens during the initial saveProgram batch.
// We can't verify the parent's `createdBy` yet, so we just check for authentication.
// This is safe because the program creation in the same batch IS verified.
!exists(/databases/$(database)/documents/programs/$(programId)) &&
request.auth != null
);
}
}
// Sessions are private records of a workout. They can be created and read
// by the user who owns them, but not updated or deleted.
match /sessions/{sessionId} {
allow read: if resource.data.userId == request.auth.uid;
allow create: if request.resource.data.userId == request.auth.uid;
allow update, delete: if false;
// Rules for the performedSets subcollection, which also requires handling batch creates.
match /performedSets/{setId} {
allow read: if get(/databases/$(database)/documents/sessions/$(sessionId)).data.userId == request.auth.uid;
allow update, delete: if false; // Performed sets are immutable records.
// The 'create' rule must handle adding sets to an existing session, or creating them
// in a batch with a new session.
allow create: if (
// Case 1: The parent session already exists. Check ownership on parent and incoming data.
exists(/databases/$(database)/documents/sessions/$(sessionId)) &&
get(/databases/$(database)/documents/sessions/$(sessionId)).data.userId == request.auth.uid &&
request.resource.data.userId == request.auth.uid
) || (
// Case 2: The parent session does not exist yet (batch write).
// We can securely validate this by checking the userId on the incoming set data itself.
!exists(/databases/$(database)/documents/sessions/$(sessionId)) &&
request.resource.data.userId == request.auth.uid
);
}
}
// This rule allows collection group queries on 'performedSets' (e.g., for analytics).
// It ensures that a user can only query for their own sets.
match /{path=**}/performedSets/{setId} {
allow list, get: if resource.data.userId == request.auth.uid;
}
}
}
This project is licensed under the terms of the LICENSE file.
Built with ❤️ by Premananda









