From e9d3c19c40dad6e2254140c5dce301e6840dbe90 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 22 Sep 2025 09:18:57 -0500 Subject: [PATCH] Pre-populate upload form with AI data --- app/admin/uploads/[uploadPath]/page.tsx | 16 +++- src/admin/AddUploadButton.tsx | 4 +- src/photo/ai/server.ts | 118 ++++++++++++++++-------- src/photo/ai/useAiImageQueries.ts | 2 +- src/photo/form/usePhotoFormParent.ts | 23 +---- 5 files changed, 98 insertions(+), 65 deletions(-) diff --git a/app/admin/uploads/[uploadPath]/page.tsx b/app/admin/uploads/[uploadPath]/page.tsx index 24b69c16..82614a73 100644 --- a/app/admin/uploads/[uploadPath]/page.tsx +++ b/app/admin/uploads/[uploadPath]/page.tsx @@ -15,6 +15,7 @@ import { import ErrorNote from '@/components/ErrorNote'; import { getRecipeTitleForData } from '@/photo/query'; import { getAlbumsWithMeta } from '@/album/query'; +import { addAiTextToFormData } from '@/photo/ai/server'; export const maxDuration = 60; @@ -29,7 +30,7 @@ export default async function UploadPage({ params, searchParams }: Params) { const { blobId, - formDataFromExif, + formDataFromExif: _formDataFromExif, imageResizedBase64: imageThumbnailBase64, shouldStripGpsData, error, @@ -40,7 +41,7 @@ export default async function UploadPage({ params, searchParams }: Params) { }); const isDataMissing = - !formDataFromExif || + !_formDataFromExif || (AI_CONTENT_GENERATION_ENABLED && !imageThumbnailBase64); if (isDataMissing && !error) { @@ -54,17 +55,22 @@ export default async function UploadPage({ params, searchParams }: Params) { uniqueRecipes, uniqueFilms, recipeTitle, + formDataFromExif, ] = await Promise.all([ getAlbumsWithMeta(), getUniqueTagsCached(), getUniqueRecipesCached(), getUniqueFilmsCached(), - formDataFromExif?.recipeData && formDataFromExif.film + _formDataFromExif?.recipeData && _formDataFromExif.film ? getRecipeTitleForData( - formDataFromExif.recipeData, - formDataFromExif.film, + _formDataFromExif.recipeData, + _formDataFromExif.film, ) : undefined, + addAiTextToFormData( + _formDataFromExif, + imageThumbnailBase64, + ), ]); const hasAiTextGeneration = AI_CONTENT_GENERATION_ENABLED; diff --git a/src/admin/AddUploadButton.tsx b/src/admin/AddUploadButton.tsx index 297ceaca..bde3a708 100644 --- a/src/admin/AddUploadButton.tsx +++ b/src/admin/AddUploadButton.tsx @@ -4,7 +4,7 @@ import { generateLocalNaivePostgresString, generateLocalPostgresString, } from '@/utility/date'; -import { pathForAdminUploadUrl } from '@/app/path'; +import { PATH_ADMIN_PHOTOS } from '@/app/path'; import { useRouter } from 'next/navigation'; import { ComponentProps, useState } from 'react'; import IconAddUpload from '@/components/icons/IconAddUpload'; @@ -42,7 +42,7 @@ export default function AddUploadButton({ }) .then(() => { if (shouldRedirectToAdminPhotos) { - router.push(pathForAdminUploadUrl(url)); + router.push(PATH_ADMIN_PHOTOS); } else { onAddFinish?.(true); setIsAddingLocal(false); diff --git a/src/photo/ai/server.ts b/src/photo/ai/server.ts index c85ca106..7c4a3d18 100644 --- a/src/photo/ai/server.ts +++ b/src/photo/ai/server.ts @@ -2,9 +2,12 @@ import { generateOpenAiImageQuery } from '@/platforms/openai'; import { AiAutoGeneratedField, getAiImageQuery, + getAiTextFieldsToGenerate, parseTitleAndCaption, } from '.'; import { getUniqueTags } from '@/photo/query'; +import { AI_TEXT_AUTO_GENERATED_FIELDS } from '@/app/config'; +import { PhotoFormData } from '../form'; export const generateAiImageQueries = async ( imageBase64?: string, @@ -26,53 +29,63 @@ export const generateAiImageQueries = async ( try { if (imageBase64) { - if ( + const shouldGenerateTitleAndCaption = textFieldsToGenerate.includes('title') && - textFieldsToGenerate.includes('caption') - ) { - const titleAndCaption = await generateOpenAiImageQuery( + textFieldsToGenerate.includes('caption'); + const shouldGenerateTitle = + !shouldGenerateTitleAndCaption && + textFieldsToGenerate.includes('title'); + const shouldGenerateCaption = + !shouldGenerateTitleAndCaption && + textFieldsToGenerate.includes('caption'); + const shouldGenerateTags = textFieldsToGenerate.includes('tags'); + const shouldGenerateSemantic = textFieldsToGenerate.includes('semantic'); + + const [ + titleAndCaption, + _title, + _caption, + _tags, + _semanticDescription, + ] = await Promise.all([ + shouldGenerateTitleAndCaption ? generateOpenAiImageQuery( imageBase64, getAiImageQuery('title-and-caption'), isBatch, - ); - if (titleAndCaption) { - const titleAndCaptionParsed = parseTitleAndCaption(titleAndCaption); - title = titleAndCaptionParsed.title; - caption = titleAndCaptionParsed.caption; - } - } else { - if (textFieldsToGenerate.includes('title')) { - title = await generateOpenAiImageQuery( - imageBase64, - getAiImageQuery('title', undefined, existingTitle), - isBatch, - ); - } - if (textFieldsToGenerate.includes('caption')) { - caption = await generateOpenAiImageQuery( - imageBase64, - getAiImageQuery('caption'), - isBatch, - ); - } - } - - if (textFieldsToGenerate.includes('tags')) { - const existingTags = await getUniqueTags(); - tags = await generateOpenAiImageQuery( + ): undefined, + shouldGenerateTitle ? generateOpenAiImageQuery( imageBase64, - getAiImageQuery('tags', existingTags), + getAiImageQuery('title', undefined, existingTitle), isBatch, - ); - } - - if (textFieldsToGenerate.includes('semantic')) { - semanticDescription = await generateOpenAiImageQuery( + ): undefined, + shouldGenerateCaption ? generateOpenAiImageQuery( + imageBase64, + getAiImageQuery('caption'), + isBatch, + ): undefined, + shouldGenerateTags ? getUniqueTags() + .then(existingTags => generateOpenAiImageQuery( + imageBase64, + getAiImageQuery('tags', existingTags), + isBatch, + )): undefined, + shouldGenerateSemantic ? generateOpenAiImageQuery( imageBase64, getAiImageQuery('description-small'), isBatch, - ); + ): undefined, + ]); + + if (titleAndCaption) { + const titleAndCaptionParsed = parseTitleAndCaption(titleAndCaption); + title = titleAndCaptionParsed.title; + caption = titleAndCaptionParsed.caption; + } else { + title = _title; + caption = _caption; } + tags = _tags; + semanticDescription = _semanticDescription; } } catch (e: any) { error = e.message; @@ -87,3 +100,34 @@ export const generateAiImageQueries = async ( error, }; }; + +export const addAiTextToFormData = async ( + formData: Partial = {}, + imageBase64?: string, + title?: string, + tags?: string, +): Promise> => { + const { + title: aiTitle, + caption: aiCaption, + tags: aiTags, + semanticDescription, + } = await generateAiImageQueries( + imageBase64, + getAiTextFieldsToGenerate( + AI_TEXT_AUTO_GENERATED_FIELDS, + Boolean(title || formData?.title), + Boolean(formData?.caption), + Boolean(tags || formData?.tags), + ), + title || formData?.title, + ); + + return { + ...formData, + title: formData?.title || aiTitle, + caption: formData?.caption || aiCaption, + tags: formData?.tags || aiTags, + semanticDescription, + }; +}; diff --git a/src/photo/ai/useAiImageQueries.ts b/src/photo/ai/useAiImageQueries.ts index c59711cc..d1b24d57 100644 --- a/src/photo/ai/useAiImageQueries.ts +++ b/src/photo/ai/useAiImageQueries.ts @@ -6,8 +6,8 @@ import { AI_AUTO_GENERATED_FIELDS_ALL, AiAutoGeneratedField } from '.'; export type AiContent = ReturnType; export default function useAiImageQueries( - textFieldsToAutoGenerate: AiAutoGeneratedField[] = [], imageBase64?: string, + textFieldsToAutoGenerate: AiAutoGeneratedField[] = [], ) { const [ requestTitleCaption, diff --git a/src/photo/form/usePhotoFormParent.ts b/src/photo/form/usePhotoFormParent.ts index bf3715ee..def379bb 100644 --- a/src/photo/form/usePhotoFormParent.ts +++ b/src/photo/form/usePhotoFormParent.ts @@ -1,7 +1,7 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useState } from 'react'; import { PhotoFormData, formHasExistingAiTextContent } from '.'; import useAiImageQueries from '../ai/useAiImageQueries'; -import { AiAutoGeneratedField, getAiTextFieldsToGenerate } from '../ai'; +import { AiAutoGeneratedField } from '../ai'; export default function usePhotoFormParent({ photoForm, @@ -24,24 +24,7 @@ export default function usePhotoFormParent({ ); }, []); - // Don't auto-generate titles when they can be captured from EXIF data - const textFieldsToAutoGenerate = useMemo(() => - getAiTextFieldsToGenerate( - _textFieldsToAutoGenerate, - Boolean(photoForm?.title), - Boolean(photoForm?.caption), - Boolean(photoForm?.tags), - ), [ - _textFieldsToAutoGenerate, - photoForm?.title, - photoForm?.caption, - photoForm?.tags, - ]); - - const aiContent = useAiImageQueries( - textFieldsToAutoGenerate, - imageThumbnailBase64, - ); + const aiContent = useAiImageQueries(imageThumbnailBase64); return { pending,