import { GoogleGenAI } from '@google/genai' import { supabase } from './supabase' import type { Database } from './supabase' type WorkoutRow = Database['public']['Tables']['workouts']['Row'] function getAI(): GoogleGenAI { const apiKey = process.env['GEMINI_API_KEY'] if (!apiKey) { throw new Error('GEMINI_API_KEY environment variable is not set') } return new GoogleGenAI({ apiKey }) } export interface GeneratedImage { buffer: Buffer mimeType: string } export interface VideoGenerationResult { workoutId: string trainerId: string videoUrl: string videoPath: string } export async function generateTrainerAvatar( prompt: string, _trainerName: string ): Promise { const config = { imageConfig: { aspectRatio: '1:1', imageSize: '1K', personGeneration: 'ALLOW_ADULT', }, responseModalities: ['IMAGE', 'TEXT'], } const response = await getAI().models.generateContentStream({ model: 'gemini-3.1-flash-image-preview', config, contents: [{ role: 'user', parts: [{ text: prompt }] }], }) const images: GeneratedImage[] = [] for await (const chunk of response) { const parts = chunk.candidates?.[0]?.content?.parts if (!parts) continue for (const part of parts) { if (part.inlineData) { images.push({ buffer: Buffer.from(part.inlineData.data || '', 'base64'), mimeType: part.inlineData.mimeType || 'image/png', }) } } } return images } export async function saveAvatarToStorage( trainerId: string, imageBuffer: Buffer, filename: string ): Promise { const path = `${trainerId}/${filename}` const { error } = await supabase.storage .from('trainer-avatars') .upload(path, imageBuffer, { contentType: 'image/png', upsert: true, }) if (error) throw new Error(`Failed to upload avatar: ${error.message}`) const { data: { publicUrl } } = supabase.storage .from('trainer-avatars') .getPublicUrl(path) return publicUrl } export async function generateWorkoutVideo( workoutId: string, trainerId: string, _trainerAvatarUrl: string, workoutTitle: string, exercises: { name: string; duration: number }[] ): Promise { const exerciseList = exercises.map(e => e.name).join(', ') const prompt = `${workoutTitle}. Exercises: ${exerciseList}. The video must loop seamlessly - start and end in the exact same standing position with arms at sides. Person should appear energetic and properly demonstrate each exercise. 16:9 aspect ratio.` const operation = await getAI().models.generateVideos({ model: 'veo-3.1-fast-generate-preview', source: { prompt }, config: { numberOfVideos: 1, aspectRatio: '16:9', resolution: '1080p', personGeneration: 'dont_allow', durationSeconds: 8, }, }) let currentOperation = operation while (!currentOperation.done) { await new Promise(resolve => setTimeout(resolve, 10000)) currentOperation = await getAI().operations.getVideosOperation({ operation: currentOperation }) } const videoUri = currentOperation.response?.generatedVideos?.[0]?.video?.uri if (!videoUri) throw new Error('No video generated') const response = await fetch(`${videoUri}&key=${process.env['GEMINI_API_KEY']}`) const arrayBuffer = await response.arrayBuffer() const videoBuffer = Buffer.from(arrayBuffer) const videoPath = `${trainerId}/${workoutId}.mp4` const { error } = await supabase.storage .from('trainers-videos') .upload(videoPath, videoBuffer, { contentType: 'video/mp4', upsert: true, }) if (error) throw new Error(`Failed to upload video: ${error.message}`) const { data: { publicUrl } } = supabase.storage .from('trainers-videos') .getPublicUrl(videoPath) return { workoutId, trainerId, videoUrl: publicUrl, videoPath } } export async function isWorkoutInProgram(_workoutId: string): Promise { // Legacy program_workouts table removed — workout programs now use program_tabatas return false } export async function isVideoGeneratedForWorkout(_workoutId: string): Promise { // Legacy program_workouts table removed — video status tracked in workouts table return false } export async function getWorkoutVideoStatus(workoutId: string): Promise<{ hasVideo: boolean videoUrl: string | null inProgram: boolean videoGenerated: boolean }> { const workoutResult = await supabase .from('workouts') .select('video_url') .eq('id', workoutId) .single() const workoutVideoUrl = (workoutResult.data as WorkoutRow | null)?.video_url ?? null return { hasVideo: !!workoutVideoUrl, videoUrl: workoutVideoUrl, inProgram: false, videoGenerated: false, } }