Auto-label recognized recipes

This commit is contained in:
Sam Becker 2025-03-06 14:41:07 -08:00
parent b114bca43e
commit 7911cf1e2e
4 changed files with 58 additions and 19 deletions

View File

@ -15,7 +15,6 @@ import { GetPhotosOptions, areOptionsSensitive } from './db';
import { import {
FIELDS_TO_NOT_OVERWRITE_WITH_NULL_DATA_ON_SYNC, FIELDS_TO_NOT_OVERWRITE_WITH_NULL_DATA_ON_SYNC,
PhotoFormData, PhotoFormData,
convertFormDataToPhotoDbInsert,
convertPhotoToFormData, convertPhotoToFormData,
} from './form'; } from './form';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
@ -34,7 +33,11 @@ import {
PATH_ROOT, PATH_ROOT,
pathForPhoto, pathForPhoto,
} from '@/app/paths'; } from '@/app/paths';
import { blurImageFromUrl, extractImageDataFromBlobPath } from './server'; import {
blurImageFromUrl,
convertFormDataToPhotoDbInsertAndLookupRecipeTitle,
extractImageDataFromBlobPath,
} from './server';
import { TAG_FAVS, isTagFavs } from '@/tag'; import { TAG_FAVS, isTagFavs } from '@/tag';
import { convertPhotoToPhotoDbInsert, Photo } from '.'; import { convertPhotoToPhotoDbInsert, Photo } from '.';
import { runAuthenticatedAdminServerAction } from '@/auth'; import { runAuthenticatedAdminServerAction } from '@/auth';
@ -59,7 +62,8 @@ export const createPhotoAction = async (formData: FormData) =>
const shouldStripGpsData = formData.get('shouldStripGpsData') === 'true'; const shouldStripGpsData = formData.get('shouldStripGpsData') === 'true';
formData.delete('shouldStripGpsData'); formData.delete('shouldStripGpsData');
const photo = convertFormDataToPhotoDbInsert(formData); const photo =
await convertFormDataToPhotoDbInsertAndLookupRecipeTitle(formData);
const updatedUrl = await convertUploadToPhoto({ const updatedUrl = await convertUploadToPhoto({
urlOrigin: photo.url, urlOrigin: photo.url,
@ -160,7 +164,8 @@ export const addAllUploadsAction = async ({
if (updatedUrl) { if (updatedUrl) {
const subheadFinal = 'Adding to database'; const subheadFinal = 'Adding to database';
streamUpdate(subheadFinal); streamUpdate(subheadFinal);
const photo = convertFormDataToPhotoDbInsert(form); const photo =
await convertFormDataToPhotoDbInsertAndLookupRecipeTitle(form);
photo.url = updatedUrl; photo.url = updatedUrl;
await insertPhoto(photo); await insertPhoto(photo);
addedUploadUrls.push(url); addedUploadUrls.push(url);
@ -185,7 +190,8 @@ export const addAllUploadsAction = async ({
export const updatePhotoAction = async (formData: FormData) => export const updatePhotoAction = async (formData: FormData) =>
runAuthenticatedAdminServerAction(async () => { runAuthenticatedAdminServerAction(async () => {
const photo = convertFormDataToPhotoDbInsert(formData); const photo =
await convertFormDataToPhotoDbInsertAndLookupRecipeTitle(formData);
let urlToDelete: string | undefined; let urlToDelete: string | undefined;
if (photo.hidden && photo.url.includes(photo.id)) { if (photo.hidden && photo.url.includes(photo.id)) {
@ -368,16 +374,17 @@ export const syncPhotoAction = async (photoId: string) =>
} }
}); });
const photoFormDbInsert = convertFormDataToPhotoDbInsert({ const photoFormDbInsert =
...formDataFromPhoto, await convertFormDataToPhotoDbInsertAndLookupRecipeTitle({
...formDataFromExif, ...formDataFromPhoto,
...!BLUR_ENABLED && { blurData: undefined }, ...formDataFromExif,
...!photo.title && { title: atTitle }, ...!BLUR_ENABLED && { blurData: undefined },
...!photo.caption && { caption: aiCaption }, ...!photo.title && { title: atTitle },
...photo.tags.length === 0 && { tags: aiTags }, ...!photo.caption && { caption: aiCaption },
...!photo.semanticDescription && ...photo.tags.length === 0 && { tags: aiTags },
{ semanticDescription: aiSemanticDescription }, ...!photo.semanticDescription &&
}); { semanticDescription: aiSemanticDescription },
});
await updatePhoto(photoFormDbInsert) await updatePhoto(photoFormDbInsert)
.then(async () => { .then(async () => {

View File

@ -187,7 +187,7 @@ export const insertPhoto = (photo: PhotoDbInsert) =>
${photo.longitude}, ${photo.longitude},
${photo.filmSimulation}, ${photo.filmSimulation},
${photo.recipeTitle}, ${photo.recipeTitle},
${JSON.stringify(photo.recipeData)}, ${photo.recipeData},
${photo.priorityOrder}, ${photo.priorityOrder},
${photo.hidden}, ${photo.hidden},
${photo.takenAt}, ${photo.takenAt},
@ -221,7 +221,7 @@ export const updatePhoto = (photo: PhotoDbInsert) =>
longitude=${photo.longitude}, longitude=${photo.longitude},
film_simulation=${photo.filmSimulation}, film_simulation=${photo.filmSimulation},
recipe_title=${photo.recipeTitle}, recipe_title=${photo.recipeTitle},
recipe_data=${JSON.stringify(photo.recipeData)}, recipe_data=${photo.recipeData},
priority_order=${photo.priorityOrder || null}, priority_order=${photo.priorityOrder || null},
hidden=${photo.hidden}, hidden=${photo.hidden},
taken_at=${photo.takenAt}, taken_at=${photo.takenAt},
@ -357,6 +357,16 @@ export const getUniqueRecipes = async () =>
}))) })))
, 'getUniqueRecipes'); , '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 () => export const getUniqueFocalLengths = async () =>
safelyQueryPhotos(() => sql` safelyQueryPhotos(() => sql`
SELECT DISTINCT focal_length, COUNT(*) SELECT DISTINCT focal_length, COUNT(*)

View File

@ -144,7 +144,10 @@ export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
exposureCompensationFormatted: exposureCompensationFormatted:
formatExposureCompensation(photoDb.exposureCompensation), formatExposureCompensation(photoDb.exposureCompensation),
recipeData: photoDb.recipeData 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, : undefined,
takenAtNaiveFormatted: takenAtNaiveFormatted:
formatDateFromPostgresString(photoDb.takenAtNaive), formatDateFromPostgresString(photoDb.takenAtNaive),

View File

@ -2,7 +2,10 @@ import {
getExtensionFromStorageUrl, getExtensionFromStorageUrl,
getIdFromStorageUrl, getIdFromStorageUrl,
} from '@/platforms/storage'; } from '@/platforms/storage';
import { convertExifToFormData } from '@/photo/form'; import {
convertExifToFormData,
convertFormDataToPhotoDbInsert,
} from '@/photo/form';
import { import {
getFujifilmSimulationFromMakerNote, getFujifilmSimulationFromMakerNote,
} from '@/platforms/fujifilm/simulation'; } from '@/platforms/fujifilm/simulation';
@ -19,6 +22,7 @@ import {
FujifilmRecipe, FujifilmRecipe,
getFujifilmRecipeFromMakerNote, getFujifilmRecipeFromMakerNote,
} from '@/platforms/fujifilm/recipe'; } from '@/platforms/fujifilm/recipe';
import { getRecipeTitleForData } from './db/query';
const IMAGE_WIDTH_RESIZE = 200; const IMAGE_WIDTH_RESIZE = 200;
const IMAGE_WIDTH_BLUR = 200; const IMAGE_WIDTH_BLUR = 200;
@ -192,3 +196,18 @@ export const removeGpsData = async (image: ArrayBuffer) =>
}) })
.toFormat('jpeg', { quality: PRESERVE_ORIGINAL_UPLOADS ? 95 : 80 }) .toFormat('jpeg', { quality: PRESERVE_ORIGINAL_UPLOADS ? 95 : 80 })
.toBuffer(); .toBuffer();
export const convertFormDataToPhotoDbInsertAndLookupRecipeTitle =
async (...args: Parameters<typeof convertFormDataToPhotoDbInsert>):
Promise<ReturnType<typeof convertFormDataToPhotoDbInsert>> => {
const photo = convertFormDataToPhotoDbInsert(...args);
if (photo.recipeData && !photo.recipeTitle) {
const recipeTitle = await getRecipeTitleForData(photo.recipeData);
if (recipeTitle) {
photo.recipeTitle = recipeTitle;
}
}
return photo;
};