11import { NextRequest } from 'next/server' ;
2- import { generateImage , NoImageGeneratedError } from 'ai' ;
3- import { createGoogleGenerativeAI } from '@ai-sdk/google' ;
4- import { getDb , getSession } from '@/lib/db' ;
5- import crypto from 'crypto' ;
6- import fs from 'fs' ;
7- import path from 'path' ;
8- import os from 'os' ;
9-
10- const dataDir = process . env . CLAUDE_GUI_DATA_DIR || path . join ( os . homedir ( ) , '.codepilot' ) ;
11- const MEDIA_DIR = path . join ( dataDir , '.codepilot-media' ) ;
2+ import { generateSingleImage , NoImageGeneratedError } from '@/lib/image-generator' ;
123
134interface GenerateRequest {
145 prompt : string ;
@@ -25,8 +16,6 @@ export const dynamic = 'force-dynamic';
2516export const maxDuration = 300 ;
2617
2718export async function POST ( request : NextRequest ) {
28- const startTime = Date . now ( ) ;
29-
3019 try {
3120 const body : GenerateRequest = await request . json ( ) ;
3221
@@ -37,156 +26,29 @@ export async function POST(request: NextRequest) {
3726 ) ;
3827 }
3928
40- const db = getDb ( ) ;
41- const provider = db . prepare (
42- "SELECT api_key FROM api_providers WHERE provider_type = 'gemini-image' AND api_key != '' LIMIT 1"
43- ) . get ( ) as { api_key : string } | undefined ;
44-
45- if ( ! provider ) {
46- return new Response (
47- JSON . stringify ( { error : 'No Gemini Image provider configured. Please add a provider with type "gemini-image" in Settings.' } ) ,
48- { status : 400 , headers : { 'Content-Type' : 'application/json' } }
49- ) ;
50- }
51-
52- const requestedModel = body . model || 'gemini-3-pro-image-preview' ;
53- const aspectRatio = ( body . aspectRatio || '1:1' ) as `${number } :${number } `;
54- const imageSize = body . imageSize || '1K' ;
55-
56- const google = createGoogleGenerativeAI ( { apiKey : provider . api_key } ) ;
57-
58- // Build prompt: plain string or { text, images } for reference images
59- // Support both base64 referenceImages and on-disk referenceImagePaths
60- let refImageData : string [ ] = [ ] ;
61- if ( body . referenceImages && body . referenceImages . length > 0 ) {
62- refImageData = body . referenceImages . map ( img => img . data ) ;
63- } else if ( body . referenceImagePaths && body . referenceImagePaths . length > 0 ) {
64- for ( const filePath of body . referenceImagePaths ) {
65- if ( fs . existsSync ( filePath ) ) {
66- const buf = fs . readFileSync ( filePath ) ;
67- refImageData . push ( buf . toString ( 'base64' ) ) ;
68- }
69- }
70- }
71- const prompt = refImageData . length > 0
72- ? { text : body . prompt , images : refImageData }
73- : body . prompt ;
74-
75- const { images } = await generateImage ( {
76- model : google . image ( requestedModel ) ,
77- prompt,
78- providerOptions : {
79- google : {
80- imageConfig : { aspectRatio, imageSize } ,
81- } ,
82- } ,
83- maxRetries : 3 ,
84- abortSignal : AbortSignal . timeout ( 120_000 ) ,
29+ const result = await generateSingleImage ( {
30+ prompt : body . prompt ,
31+ model : body . model ,
32+ aspectRatio : body . aspectRatio ,
33+ imageSize : body . imageSize ,
34+ referenceImages : body . referenceImages ,
35+ referenceImagePaths : body . referenceImagePaths ,
36+ sessionId : body . sessionId ,
8537 } ) ;
8638
87- const elapsed = Date . now ( ) - startTime ;
88- console . log ( `[media/generate] ${ requestedModel } ${ imageSize } completed in ${ elapsed } ms` ) ;
89-
90- // Ensure media directory exists
91- if ( ! fs . existsSync ( MEDIA_DIR ) ) {
92- fs . mkdirSync ( MEDIA_DIR , { recursive : true } ) ;
93- }
94-
95- // Write images to disk
96- const savedImages : Array < { mimeType : string ; localPath : string } > = [ ] ;
97-
98- for ( const img of images ) {
99- const ext = img . mediaType === 'image/jpeg' ? '.jpg'
100- : img . mediaType === 'image/webp' ? '.webp'
101- : '.png' ;
102- const filename = `${ Date . now ( ) } -${ crypto . randomBytes ( 8 ) . toString ( 'hex' ) } ${ ext } ` ;
103- const filePath = path . join ( MEDIA_DIR , filename ) ;
104-
105- fs . writeFileSync ( filePath , Buffer . from ( img . uint8Array ) ) ;
106-
107- savedImages . push ( {
108- mimeType : img . mediaType ,
109- localPath : filePath ,
110- } ) ;
111- }
112-
113- // Copy images to project directory if sessionId is provided
114- if ( body . sessionId ) {
115- try {
116- const session = getSession ( body . sessionId ) ;
117- if ( session ?. working_directory ) {
118- const projectImgDir = path . join ( session . working_directory , '.codepilot-images' ) ;
119- if ( ! fs . existsSync ( projectImgDir ) ) {
120- fs . mkdirSync ( projectImgDir , { recursive : true } ) ;
121- }
122- for ( const saved of savedImages ) {
123- const destPath = path . join ( projectImgDir , path . basename ( saved . localPath ) ) ;
124- fs . copyFileSync ( saved . localPath , destPath ) ;
125- }
126- console . log ( `[media/generate] Copied ${ savedImages . length } image(s) to ${ projectImgDir } ` ) ;
127- }
128- } catch ( copyErr ) {
129- console . warn ( '[media/generate] Failed to copy images to project directory:' , copyErr ) ;
130- }
131- }
132-
133- // Save reference images to disk for gallery display
134- const savedRefImages : Array < { mimeType : string ; localPath : string } > = [ ] ;
135- if ( refImageData . length > 0 ) {
136- const refMimeTypes = body . referenceImages
137- ? body . referenceImages . map ( img => img . mimeType )
138- : body . referenceImagePaths
139- ? body . referenceImagePaths . map ( ( ) => 'image/png' )
140- : [ ] ;
141- for ( let i = 0 ; i < refImageData . length ; i ++ ) {
142- const mime = refMimeTypes [ i ] || 'image/png' ;
143- const ext = mime === 'image/jpeg' ? '.jpg' : mime === 'image/webp' ? '.webp' : '.png' ;
144- const filename = `ref-${ Date . now ( ) } -${ crypto . randomBytes ( 4 ) . toString ( 'hex' ) } ${ ext } ` ;
145- const filePath = path . join ( MEDIA_DIR , filename ) ;
146- fs . writeFileSync ( filePath , Buffer . from ( refImageData [ i ] , 'base64' ) ) ;
147- savedRefImages . push ( { mimeType : mime , localPath : filePath } ) ;
148- }
149- }
150-
151- // DB record
152- const id = crypto . randomBytes ( 16 ) . toString ( 'hex' ) ;
153- const now = new Date ( ) . toISOString ( ) . replace ( 'T' , ' ' ) . split ( '.' ) [ 0 ] ;
154- const localPath = savedImages . length > 0 ? savedImages [ 0 ] . localPath : '' ;
155-
156- const metadata : Record < string , unknown > = {
157- imageCount : savedImages . length ,
158- elapsedMs : elapsed ,
159- model : requestedModel ,
160- } ;
161- if ( savedRefImages . length > 0 ) {
162- metadata . referenceImages = savedRefImages ;
163- }
164-
165- db . prepare (
166- `INSERT INTO media_generations (id, type, status, provider, model, prompt, aspect_ratio, image_size, local_path, thumbnail_path, session_id, message_id, tags, metadata, error, created_at, completed_at)
167- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
168- ) . run (
169- id , 'image' , 'completed' , 'gemini' , requestedModel , body . prompt ,
170- aspectRatio , imageSize , localPath , '' ,
171- body . sessionId || null , null ,
172- '[]' , JSON . stringify ( metadata ) ,
173- null , now , now
174- ) ;
175-
17639 return new Response (
17740 JSON . stringify ( {
178- id,
41+ id : result . mediaGenerationId ,
17942 text : '' ,
180- images : savedImages ,
181- model : requestedModel ,
182- imageSize,
183- elapsedMs : elapsed ,
43+ images : result . images ,
44+ model : body . model || 'gemini-3.1-flash-image-preview' ,
45+ imageSize : body . imageSize || '1K' ,
46+ elapsedMs : result . elapsedMs ,
18447 } ) ,
18548 { status : 200 , headers : { 'Content-Type' : 'application/json' } }
18649 ) ;
18750 } catch ( error ) {
188- const elapsed = Date . now ( ) - startTime ;
189- console . error ( `[media/generate] Failed after ${ elapsed } ms:` , error ) ;
51+ console . error ( '[media/generate] Failed:' , error ) ;
19052
19153 if ( NoImageGeneratedError . isInstance ( error ) ) {
19254 return new Response (
0 commit comments