@ -365,7 +365,7 @@ async function generateWithReplicate(
} ;
} ;
}
}
// Extract output image(s)
// Extract output
const output = currentPrediction . output ;
const output = currentPrediction . output ;
if ( ! output ) {
if ( ! output ) {
return {
return {
@ -380,36 +380,60 @@ async function generateWithReplicate(
if ( outputUrls . length === 0 ) {
if ( outputUrls . length === 0 ) {
return {
return {
success : false ,
success : false ,
error : "No output images from prediction" ,
error : "No output from prediction" ,
} ;
} ;
}
}
// Fetch the first output image and convert to base64
// Fetch the first output and convert to base64
const i mag eUrl = outputUrls [ 0 ] ;
const media Url = outputUrls [ 0 ] ;
console . log ( ` [API: ${ requestId } ] Fetching output image from: ${ i mag eUrl} ` ) ;
console . log ( ` [API: ${ requestId } ] Fetching output from: ${ media Url } ` ) ;
const i mag eResponse = await fetch ( i mag eUrl) ;
const media Response = await fetch ( media Url ) ;
if ( ! i mag eResponse. ok ) {
if ( ! media Response . ok ) {
return {
return {
success : false ,
success : false ,
error : ` Failed to fetch output image : ${ i mag eResponse. status } ` ,
error : ` Failed to fetch output: ${ media Response . status } ` ,
} ;
} ;
}
}
const imageArrayBuffer = await imageResponse . arrayBuffer ( ) ;
const imageBase64 = Buffer . from ( imageArrayBuffer ) . toString ( "base64" ) ;
// Determine MIME type from response
// Determine MIME type from response
const contentType = imageResponse . headers . get ( "content-type" ) || "image/png" ;
const contentType = mediaResponse . headers . get ( "content-type" ) || "image/png" ;
const isVideo = contentType . startsWith ( "video/" ) ;
const mediaArrayBuffer = await mediaResponse . arrayBuffer ( ) ;
const mediaSizeBytes = mediaArrayBuffer . byteLength ;
const mediaSizeMB = mediaSizeBytes / ( 1024 * 1024 ) ;
// Log warning for large files
if ( mediaSizeMB > 10 ) {
console . warn ( ` [API: ${ requestId } ] Large output file: ${ mediaSizeMB . toFixed ( 2 ) } MB ` ) ;
}
// For very large videos (>20MB), return URL directly instead of base64
if ( isVideo && mediaSizeMB > 20 ) {
console . log ( ` [API: ${ requestId } ] Replicate video generation successful (URL only, too large for base64) ` ) ;
return {
success : true ,
outputs : [
{
type : "video" ,
data : mediaUrl , // Return URL directly for very large videos
url : mediaUrl ,
} ,
] ,
} ;
}
console . log ( ` [API: ${ requestId } ] Replicate generation successful ` ) ;
const mediaBase64 = Buffer . from ( mediaArrayBuffer ) . toString ( "base64" ) ;
console . log ( ` [API: ${ requestId } ] Replicate ${ isVideo ? "video" : "image" } generation successful ` ) ;
return {
return {
success : true ,
success : true ,
outputs : [
outputs : [
{
{
type : "image" ,
type : isVideo ? "video" : "image",
data : ` data: ${ contentType } ;base64, ${ imageBase64 } ` ,
data : ` data: ${ contentType } ;base64, ${ media Base64 } ` ,
url : imageUrl ,
url : media Url ,
} ,
} ,
] ,
] ,
} ;
} ;
@ -470,51 +494,85 @@ async function generateWithFal(
const result = await response . json ( ) ;
const result = await response . json ( ) ;
// fal.ai response typically has "images" array with url field
// fal.ai response can have different structures:
// or "image" object with url field depending on the model
// - images: array with url field (image models)
let imageUrl : string | null = null ;
// - image: object with url field (image models)
// - video: object with url field (video models)
if ( result . images && Array . isArray ( result . images ) && result . images . length > 0 ) {
// - output: string URL (some models)
imageUrl = result . images [ 0 ] . url ;
let mediaUrl : string | null = null ;
let isVideoModel = false ;
// Check for video output first (video models)
if ( result . video && result . video . url ) {
mediaUrl = result . video . url ;
isVideoModel = true ;
console . log ( ` [API: ${ requestId } ] Found video URL in response ` ) ;
} else if ( result . images && Array . isArray ( result . images ) && result . images . length > 0 ) {
mediaUrl = result . images [ 0 ] . url ;
} else if ( result . image && result . image . url ) {
} else if ( result . image && result . image . url ) {
imageUrl = result . image . url ;
media Url = result . image . url ;
} else if ( result . output && typeof result . output === "string" ) {
} else if ( result . output && typeof result . output === "string" ) {
// Some models return URL directly in output
// Some models return URL directly in output
imageUrl = result . output ;
media Url = result . output ;
}
}
if ( ! imageUrl ) {
if ( ! mediaUrl ) {
console . error ( ` [API: ${ requestId } ] No media URL found in fal.ai response: ` , JSON . stringify ( result , null , 2 ) ) ;
return {
return {
success : false ,
success : false ,
error : "No i mag e URL in response" ,
error : "No media URL in response" ,
} ;
} ;
}
}
// Fetch the i mag e and convert to base64
// Fetch the media and convert to base64
console . log ( ` [API: ${ requestId } ] Fetching output image from: ${ i mag eUrl} ` ) ;
console . log ( ` [API: ${ requestId } ] Fetching output from: ${ media Url } ` ) ;
const i mag eResponse = await fetch ( i mag eUrl) ;
const media Response = await fetch ( media Url ) ;
if ( ! i mag eResponse. ok ) {
if ( ! media Response . ok ) {
return {
return {
success : false ,
success : false ,
error : ` Failed to fetch output image : ${ i mag eResponse. status } ` ,
error : ` Failed to fetch output: ${ media Response . status } ` ,
} ;
} ;
}
}
const imageArrayBuffer = await imageResponse . arrayBuffer ( ) ;
const imageBase64 = Buffer . from ( imageArrayBuffer ) . toString ( "base64" ) ;
// Determine MIME type from response
// Determine MIME type from response
const contentType = imageResponse . headers . get ( "content-type" ) || "image/png" ;
const contentType = mediaResponse . headers . get ( "content-type" ) || ( isVideoModel ? "video/mp4" : "image/png" ) ;
const isVideo = contentType . startsWith ( "video/" ) || isVideoModel ;
console . log ( ` [API: ${ requestId } ] fal.ai generation successful ` ) ;
const mediaArrayBuffer = await mediaResponse . arrayBuffer ( ) ;
const mediaSizeBytes = mediaArrayBuffer . byteLength ;
const mediaSizeMB = mediaSizeBytes / ( 1024 * 1024 ) ;
// Log warning for large files
if ( mediaSizeMB > 10 ) {
console . warn ( ` [API: ${ requestId } ] Large output file: ${ mediaSizeMB . toFixed ( 2 ) } MB ` ) ;
}
// For very large videos (>20MB), return URL directly instead of base64
if ( isVideo && mediaSizeMB > 20 ) {
console . log ( ` [API: ${ requestId } ] fal.ai video generation successful (URL only, too large for base64) ` ) ;
return {
success : true ,
outputs : [
{
type : "video" ,
data : mediaUrl , // Return URL directly for very large videos
url : mediaUrl ,
} ,
] ,
} ;
}
const mediaBase64 = Buffer . from ( mediaArrayBuffer ) . toString ( "base64" ) ;
console . log ( ` [API: ${ requestId } ] fal.ai ${ isVideo ? "video" : "image" } generation successful ` ) ;
return {
return {
success : true ,
success : true ,
outputs : [
outputs : [
{
{
type : "image" ,
type : isVideo ? "video" : "image",
data : ` data: ${ contentType } ;base64, ${ imageBase64 } ` ,
data : ` data: ${ contentType } ;base64, ${ media Base64 } ` ,
url : imageUrl ,
url : media Url ,
} ,
} ,
] ,
] ,
} ;
} ;
@ -614,21 +672,34 @@ export async function POST(request: NextRequest) {
) ;
) ;
}
}
// Return first output image
// Return first output ( image or video)
const outputImage = result . outputs ? . [ 0 ] ? . data ;
const output = result . outputs ? . [ 0 ] ;
if ( ! outputImage ) {
if ( ! output ? . data ) {
return NextResponse . json < GenerateResponse > (
return NextResponse . json < GenerateResponse > (
{
{
success : false ,
success : false ,
error : "No image in generation outpu t" ,
error : "No output in generation resul t" ,
} ,
} ,
{ status : 500 }
{ status : 500 }
) ;
) ;
}
}
// Return appropriate fields based on output type
if ( output . type === "video" ) {
// Check if data is a URL (for large videos) or base64
const isUrl = output . data . startsWith ( "http" ) ;
return NextResponse . json < GenerateResponse > ( {
success : true ,
video : isUrl ? undefined : output . data ,
videoUrl : isUrl ? output.data : undefined ,
contentType : "video" ,
} ) ;
}
return NextResponse . json < GenerateResponse > ( {
return NextResponse . json < GenerateResponse > ( {
success : true ,
success : true ,
image : outputImage ,
image : output.data ,
contentType : "image" ,
} ) ;
} ) ;
} finally {
} finally {
// Clean up uploaded images
// Clean up uploaded images
@ -689,21 +760,34 @@ export async function POST(request: NextRequest) {
) ;
) ;
}
}
// Return first output image
// Return first output ( image or video)
const outputImage = result . outputs ? . [ 0 ] ? . data ;
const output = result . outputs ? . [ 0 ] ;
if ( ! outputImage ) {
if ( ! output ? . data ) {
return NextResponse . json < GenerateResponse > (
return NextResponse . json < GenerateResponse > (
{
{
success : false ,
success : false ,
error : "No image in generation outpu t" ,
error : "No output in generation resul t" ,
} ,
} ,
{ status : 500 }
{ status : 500 }
) ;
) ;
}
}
// Return appropriate fields based on output type
if ( output . type === "video" ) {
// Check if data is a URL (for large videos) or base64
const isUrl = output . data . startsWith ( "http" ) ;
return NextResponse . json < GenerateResponse > ( {
success : true ,
video : isUrl ? undefined : output . data ,
videoUrl : isUrl ? output.data : undefined ,
contentType : "video" ,
} ) ;
}
return NextResponse . json < GenerateResponse > ( {
return NextResponse . json < GenerateResponse > ( {
success : true ,
success : true ,
image : outputImage ,
image : output.data ,
contentType : "image" ,
} ) ;
} ) ;
} finally {
} finally {
// Clean up uploaded images
// Clean up uploaded images