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 {
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 () => {

View File

@ -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(*)

View File

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

View File

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