Pre-populate upload form with AI data

This commit is contained in:
Sam Becker 2025-09-22 09:18:57 -05:00
parent 47fe1cf383
commit e9d3c19c40
5 changed files with 98 additions and 65 deletions

View File

@ -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;

View File

@ -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);

View File

@ -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<PhotoFormData> = {},
imageBase64?: string,
title?: string,
tags?: string,
): Promise<Partial<PhotoFormData>> => {
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,
};
};

View File

@ -6,8 +6,8 @@ import { AI_AUTO_GENERATED_FIELDS_ALL, AiAutoGeneratedField } from '.';
export type AiContent = ReturnType<typeof useAiImageQueries>;
export default function useAiImageQueries(
textFieldsToAutoGenerate: AiAutoGeneratedField[] = [],
imageBase64?: string,
textFieldsToAutoGenerate: AiAutoGeneratedField[] = [],
) {
const [
requestTitleCaption,

View File

@ -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,