2026-04-29 06:25:42 +00:00
import express from 'express' ;
import path from 'path' ;
import { fileURLToPath } from 'url' ;
import dotenv from 'dotenv' ;
2026-04-29 11:50:44 -05:00
import fs from 'fs/promises' ;
import { existsSync } from 'fs' ;
import crypto from 'crypto' ;
import { execFile } from 'child_process' ;
import util from 'util' ;
import { GoogleGenerativeAI } from '@google/generative-ai' ;
import { GoogleAIFileManager } from '@google/generative-ai/server' ;
const execFilePromise = util . promisify ( execFile ) ;
2026-04-29 06:25:42 +00:00
dotenv . config ( ) ;
const _ _filename = fileURLToPath ( import . meta . url ) ;
const _ _dirname = path . dirname ( _ _filename ) ;
2026-04-29 11:50:44 -05:00
const RECIPES _DIR = path . join ( _ _dirname , 'recipes' ) ;
const TEMP _DIR = path . join ( _ _dirname , 'temp_ingest' ) ;
// Ensure directories exist
async function ensureDirs ( ) {
try { await fs . access ( RECIPES _DIR ) ; } catch { await fs . mkdir ( RECIPES _DIR ) ; }
try { await fs . access ( TEMP _DIR ) ; } catch { await fs . mkdir ( TEMP _DIR ) ; }
}
ensureDirs ( ) ;
2026-04-29 06:25:42 +00:00
const app = express ( ) ;
app . use ( express . json ( ) ) ;
// Serve static frontend files from Vite build
app . use ( express . static ( path . join ( _ _dirname , 'dist' ) ) ) ;
2026-04-29 11:50:44 -05:00
// Secure credentials strictly from environment variables
const VALID _USER = process . env . APP _USERNAME ;
const VALID _PASS = process . env . APP _PASSWORD ;
2026-04-29 06:25:42 +00:00
const GEMINI _API _KEY = process . env . VITE _GEMINI _API _KEY || process . env . GEMINI _API _KEY ;
app . post ( '/api/login' , ( req , res ) => {
const { username , password } = req . body ;
if ( username === VALID _USER && password === VALID _PASS ) {
res . json ( { success : true } ) ;
} else {
res . status ( 401 ) . json ( { error : 'Invalid credentials' } ) ;
}
} ) ;
app . post ( '/api/generate' , async ( req , res ) => {
const { username , password , userPrompt , systemPrompt } = req . body ;
if ( username !== VALID _USER || password !== VALID _PASS ) {
return res . status ( 401 ) . json ( { error : 'Unauthorized' } ) ;
}
if ( ! GEMINI _API _KEY ) {
return res . status ( 500 ) . json ( { error : 'API key not configured on server' } ) ;
}
try {
const resp = await fetch ( ` https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key= ${ GEMINI _API _KEY } ` , {
method : 'POST' ,
headers : { 'content-type' : 'application/json' } ,
body : JSON . stringify ( {
systemInstruction : { parts : [ { text : systemPrompt } ] } ,
contents : [ { parts : [ { text : userPrompt } ] } ] ,
generationConfig : { responseMimeType : "application/json" }
} )
} ) ;
const data = await resp . json ( ) ;
if ( ! resp . ok ) {
throw new Error ( data . error ? . message || ` API error ${ resp . status } ` ) ;
}
res . json ( data ) ;
} catch ( err ) {
res . status ( 500 ) . json ( { error : err . message } ) ;
}
} ) ;
2026-04-29 11:50:44 -05:00
app . post ( '/api/ingest' , async ( req , res ) => {
const { username , password , url } = req . body ;
if ( username !== VALID _USER || password !== VALID _PASS ) {
return res . status ( 401 ) . json ( { error : 'Unauthorized' } ) ;
}
if ( ! GEMINI _API _KEY ) {
return res . status ( 500 ) . json ( { error : 'API key not configured on server' } ) ;
}
if ( ! url ) return res . status ( 400 ) . json ( { error : 'Reel URL is required' } ) ;
const jobId = crypto . randomBytes ( 8 ) . toString ( 'hex' ) ;
const jobDir = path . join ( TEMP _DIR , jobId ) ;
try {
await fs . mkdir ( jobDir , { recursive : true } ) ;
console . log ( ` [Ingest ${ jobId } ] Step 1: Running yt-dlp on ${ url } ` ) ;
const videoOutputTemplate = path . join ( jobDir , 'video.%(ext)s' ) ;
const descFile = path . join ( jobDir , 'video.description' ) ;
await execFilePromise ( 'yt-dlp' , [
'-f' , 'best[ext=mp4]/best' ,
'--no-playlist' ,
'-o' , videoOutputTemplate ,
'--write-description' ,
url
] ) ;
const dirFiles = await fs . readdir ( jobDir ) ;
const videoFile = dirFiles . find ( f => / ^ video \ . ( mp4 | webm | mkv | mov | avi ) $ / i . test ( f ) ) ;
if ( ! videoFile ) throw new Error ( 'yt-dlp did not produce a video file' ) ;
const videoPath = path . join ( jobDir , videoFile ) ;
const ext = path . extname ( videoFile ) . slice ( 1 ) . toLowerCase ( ) ;
const MIME _MAP = { mp4 : 'video/mp4' , webm : 'video/webm' , mkv : 'video/x-matroska' , mov : 'video/quicktime' , avi : 'video/x-msvideo' } ;
const videoMimeType = MIME _MAP [ ext ] || 'video/mp4' ;
let captionText = '' ;
if ( existsSync ( descFile ) ) captionText = await fs . readFile ( descFile , 'utf-8' ) ;
console . log ( ` [Ingest ${ jobId } ] Step 2: Uploading file to Gemini ( ${ videoFile } )... ` ) ;
const fileManager = new GoogleAIFileManager ( GEMINI _API _KEY ) ;
const uploadResult = await fileManager . uploadFile ( videoPath , {
mimeType : videoMimeType ,
displayName : ` Reel Ingest ${ jobId } ` ,
} ) ;
let file = await fileManager . getFile ( uploadResult . file . name ) ;
console . log ( ` [Ingest ${ jobId } ] Uploaded ${ file . displayName } as: ${ file . uri } ` ) ;
// Wait for the video processing to complete on Google's side
while ( file . state === "PROCESSING" ) {
process . stdout . write ( "." ) ;
await new Promise ( ( resolve ) => setTimeout ( resolve , 5000 ) ) ;
file = await fileManager . getFile ( uploadResult . file . name ) ;
}
if ( file . state === "FAILED" ) {
throw new Error ( "Video processing failed inside Gemini." ) ;
}
console . log ( ` \n [Ingest ${ jobId } ] Step 3: Sending prompt to Gemini... ` ) ;
const genAI = new GoogleGenerativeAI ( GEMINI _API _KEY ) ;
const model = genAI . getGenerativeModel ( { model : "gemini-2.5-flash" } ) ;
const systemPrompt = "You are a professional chef. You consume video, audio, and captions from a recipe reel, and output structured JSON. Required keys: title (string), description (string), servings (number), prepTime (string), cookTime (string), ingredients (array of {amount, unit, name} objects), steps (array of {title, instruction} objects), and notes (string)." ;
const result = await model . generateContent ( {
contents : [
{
role : 'user' , parts : [
{ fileData : { fileUri : uploadResult . file . uri , mimeType : uploadResult . file . mimeType } } ,
{ text : "Extract the recipe (ingredients, measurements, and steps) into the required JSON format." } ,
... ( captionText . trim ( ) ? [ { text : ` Original Social Media Caption: \n " ${ captionText } " \n ` } ] : [ ] )
]
}
] ,
systemInstruction : { role : 'system' , parts : [ { text : systemPrompt } ] } ,
generationConfig : { responseMimeType : "application/json" , temperature : 0.2 }
} ) ;
const recipeRaw = JSON . parse ( result . response . text ( ) ) ;
// Save securely
const recipeId = crypto . randomBytes ( 8 ) . toString ( 'hex' ) ;
await fs . writeFile ( path . join ( RECIPES _DIR , ` ${ recipeId } .json ` ) , JSON . stringify ( recipeRaw , null , 2 ) ) ;
// Cleanup local temp dir and remote API file
await fs . rm ( jobDir , { recursive : true , force : true } ) ;
await fileManager . deleteFile ( uploadResult . file . name ) ;
res . json ( { id : recipeId , recipe : recipeRaw } ) ;
} catch ( err ) {
console . error ( ` [Ingest ${ jobId } ] Failed ` , err ) ;
// Try to cleanup jobDir just in case
try { await fs . rm ( jobDir , { recursive : true , force : true } ) ; } catch { }
res . status ( 500 ) . json ( { error : err . message } ) ;
}
} ) ;
app . post ( '/api/recipes' , async ( req , res ) => {
const { username , password , recipe } = req . body ;
if ( username !== VALID _USER || password !== VALID _PASS ) {
return res . status ( 401 ) . json ( { error : 'Unauthorized' } ) ;
}
try {
const id = crypto . randomBytes ( 8 ) . toString ( 'hex' ) ;
await fs . writeFile ( path . join ( RECIPES _DIR , ` ${ id } .json ` ) , JSON . stringify ( recipe ) ) ;
res . json ( { id } ) ;
} catch ( err ) {
res . status ( 500 ) . json ( { error : 'Failed to save recipe' } ) ;
}
} ) ;
app . get ( '/api/recipes' , async ( req , res ) => {
// Pass auth via headers
const username = req . headers [ 'x-username' ] ;
const password = req . headers [ 'x-password' ] ;
if ( username !== VALID _USER || password !== VALID _PASS ) {
return res . status ( 401 ) . json ( { error : 'Unauthorized' } ) ;
}
try {
const files = await fs . readdir ( RECIPES _DIR ) ;
const recipes = [ ] ;
for ( const file of files ) {
if ( file . endsWith ( '.json' ) ) {
const content = await fs . readFile ( path . join ( RECIPES _DIR , file ) , 'utf-8' ) ;
const parsed = JSON . parse ( content ) ;
parsed . id = file . replace ( '.json' , '' ) ;
recipes . push ( parsed ) ;
}
}
res . json ( recipes ) ;
} catch ( err ) {
res . status ( 500 ) . json ( { error : 'Failed to fetch recipes' } ) ;
}
} ) ;
app . get ( '/api/recipes/:id' , async ( req , res ) => {
// Public route - anyone with the ID can view
try {
const recipeId = req . params . id . replace ( /[^a-z0-9]/gi , '' ) ; // basic sanitization
const content = await fs . readFile ( path . join ( RECIPES _DIR , ` ${ recipeId } .json ` ) , 'utf-8' ) ;
res . json ( JSON . parse ( content ) ) ;
} catch ( err ) {
res . status ( 404 ) . json ( { error : 'Recipe not found' } ) ;
}
} ) ;
2026-04-29 06:25:42 +00:00
// For any other route, send the frontend
app . get ( '*' , ( req , res ) => {
res . sendFile ( path . join ( _ _dirname , 'dist' , 'index.html' ) ) ;
} ) ;
2026-04-29 11:50:44 -05:00
const PORT = process . env . PORT || 3000 ;
2026-04-29 06:25:42 +00:00
app . listen ( PORT , ( ) => {
console . log ( ` Secure proxy server running on port ${ PORT } ` ) ;
} ) ;