From 7911cf1e2e60ee04d7ac377636b10522dfdafaf7 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Thu, 6 Mar 2025 14:41:07 -0800 Subject: [PATCH] Auto-label recognized recipes --- src/photo/actions.ts | 37 ++++++++++++++++++++++--------------- src/photo/db/query.ts | 14 ++++++++++++-- src/photo/index.ts | 5 ++++- src/photo/server.ts | 21 ++++++++++++++++++++- 4 files changed, 58 insertions(+), 19 deletions(-) diff --git a/src/photo/actions.ts b/src/photo/actions.ts index 2f038308..79315b10 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -15,7 +15,6 @@ import { GetPhotosOptions, areOptionsSensitive } from './db'; import { FIELDS_TO_NOT_OVERWRITE_WITH_NULL_DATA_ON_SYNC, PhotoFormData, - convertFormDataToPhotoDbInsert, convertPhotoToFormData, } from './form'; import { redirect } from 'next/navigation'; @@ -34,7 +33,11 @@ import { PATH_ROOT, pathForPhoto, } from '@/app/paths'; -import { blurImageFromUrl, extractImageDataFromBlobPath } from './server'; +import { + blurImageFromUrl, + convertFormDataToPhotoDbInsertAndLookupRecipeTitle, + extractImageDataFromBlobPath, +} from './server'; import { TAG_FAVS, isTagFavs } from '@/tag'; import { convertPhotoToPhotoDbInsert, Photo } from '.'; import { runAuthenticatedAdminServerAction } from '@/auth'; @@ -59,7 +62,8 @@ export const createPhotoAction = async (formData: FormData) => const shouldStripGpsData = formData.get('shouldStripGpsData') === 'true'; formData.delete('shouldStripGpsData'); - const photo = convertFormDataToPhotoDbInsert(formData); + const photo = + await convertFormDataToPhotoDbInsertAndLookupRecipeTitle(formData); const updatedUrl = await convertUploadToPhoto({ urlOrigin: photo.url, @@ -160,7 +164,8 @@ export const addAllUploadsAction = async ({ if (updatedUrl) { const subheadFinal = 'Adding to database'; streamUpdate(subheadFinal); - const photo = convertFormDataToPhotoDbInsert(form); + const photo = + await convertFormDataToPhotoDbInsertAndLookupRecipeTitle(form); photo.url = updatedUrl; await insertPhoto(photo); addedUploadUrls.push(url); @@ -185,7 +190,8 @@ export const addAllUploadsAction = async ({ export const updatePhotoAction = async (formData: FormData) => runAuthenticatedAdminServerAction(async () => { - const photo = convertFormDataToPhotoDbInsert(formData); + const photo = + await convertFormDataToPhotoDbInsertAndLookupRecipeTitle(formData); let urlToDelete: string | undefined; if (photo.hidden && photo.url.includes(photo.id)) { @@ -368,16 +374,17 @@ export const syncPhotoAction = async (photoId: string) => } }); - const photoFormDbInsert = convertFormDataToPhotoDbInsert({ - ...formDataFromPhoto, - ...formDataFromExif, - ...!BLUR_ENABLED && { blurData: undefined }, - ...!photo.title && { title: atTitle }, - ...!photo.caption && { caption: aiCaption }, - ...photo.tags.length === 0 && { tags: aiTags }, - ...!photo.semanticDescription && - { semanticDescription: aiSemanticDescription }, - }); + const photoFormDbInsert = + await convertFormDataToPhotoDbInsertAndLookupRecipeTitle({ + ...formDataFromPhoto, + ...formDataFromExif, + ...!BLUR_ENABLED && { blurData: undefined }, + ...!photo.title && { title: atTitle }, + ...!photo.caption && { caption: aiCaption }, + ...photo.tags.length === 0 && { tags: aiTags }, + ...!photo.semanticDescription && + { semanticDescription: aiSemanticDescription }, + }); await updatePhoto(photoFormDbInsert) .then(async () => { diff --git a/src/photo/db/query.ts b/src/photo/db/query.ts index 044ec829..efd75ea5 100644 --- a/src/photo/db/query.ts +++ b/src/photo/db/query.ts @@ -187,7 +187,7 @@ export const insertPhoto = (photo: PhotoDbInsert) => ${photo.longitude}, ${photo.filmSimulation}, ${photo.recipeTitle}, - ${JSON.stringify(photo.recipeData)}, + ${photo.recipeData}, ${photo.priorityOrder}, ${photo.hidden}, ${photo.takenAt}, @@ -221,7 +221,7 @@ export const updatePhoto = (photo: PhotoDbInsert) => longitude=${photo.longitude}, film_simulation=${photo.filmSimulation}, recipe_title=${photo.recipeTitle}, - recipe_data=${JSON.stringify(photo.recipeData)}, + recipe_data=${photo.recipeData}, priority_order=${photo.priorityOrder || null}, hidden=${photo.hidden}, taken_at=${photo.takenAt}, @@ -357,6 +357,16 @@ export const getUniqueRecipes = async () => }))) , 'getUniqueRecipes'); +export const getRecipeTitleForData = async (data: string | object) => + safelyQueryPhotos(() => query( + // eslint-disable-next-line max-len + 'SELECT recipe_title FROM photos WHERE hidden IS NOT TRUE AND recipe_data = $1 LIMIT 1', + // Legacy check on escaped, string-based JSON + [typeof data === 'string' ? data : JSON.stringify(data)], + ) + .then(({ rows }) => rows[0]?.recipe_title as string | undefined) + , 'getRecipeTitleForData'); + export const getUniqueFocalLengths = async () => safelyQueryPhotos(() => sql` SELECT DISTINCT focal_length, COUNT(*) diff --git a/src/photo/index.ts b/src/photo/index.ts index e7c51031..70defbbf 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -144,7 +144,10 @@ export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => { exposureCompensationFormatted: formatExposureCompensation(photoDb.exposureCompensation), recipeData: photoDb.recipeData - ? JSON.parse(photoDb.recipeData) + // Legacy check on escaped, string-based JSON + ? typeof photoDb.recipeData === 'string' + ? JSON.parse(photoDb.recipeData) + : photoDb.recipeData : undefined, takenAtNaiveFormatted: formatDateFromPostgresString(photoDb.takenAtNaive), diff --git a/src/photo/server.ts b/src/photo/server.ts index 202c4a44..adcdd54e 100644 --- a/src/photo/server.ts +++ b/src/photo/server.ts @@ -2,7 +2,10 @@ import { getExtensionFromStorageUrl, getIdFromStorageUrl, } from '@/platforms/storage'; -import { convertExifToFormData } from '@/photo/form'; +import { + convertExifToFormData, + convertFormDataToPhotoDbInsert, +} from '@/photo/form'; import { getFujifilmSimulationFromMakerNote, } from '@/platforms/fujifilm/simulation'; @@ -19,6 +22,7 @@ import { FujifilmRecipe, getFujifilmRecipeFromMakerNote, } from '@/platforms/fujifilm/recipe'; +import { getRecipeTitleForData } from './db/query'; const IMAGE_WIDTH_RESIZE = 200; const IMAGE_WIDTH_BLUR = 200; @@ -192,3 +196,18 @@ export const removeGpsData = async (image: ArrayBuffer) => }) .toFormat('jpeg', { quality: PRESERVE_ORIGINAL_UPLOADS ? 95 : 80 }) .toBuffer(); + +export const convertFormDataToPhotoDbInsertAndLookupRecipeTitle = + async (...args: Parameters): + Promise> => { + const photo = convertFormDataToPhotoDbInsert(...args); + + if (photo.recipeData && !photo.recipeTitle) { + const recipeTitle = await getRecipeTitleForData(photo.recipeData); + if (recipeTitle) { + photo.recipeTitle = recipeTitle; + } + } + + return photo; + };