Finalize multi-image upload backend data processing
This commit is contained in:
parent
3039076e27
commit
31396b83cc
@ -4,7 +4,7 @@ import ErrorNote from '@/components/ErrorNote';
|
||||
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
|
||||
import InfoBlock from '@/components/InfoBlock';
|
||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||
import { addAllUploads } from '@/photo/actions';
|
||||
import { addAllUploadsAction } from '@/photo/actions';
|
||||
import { PATH_ADMIN_PHOTOS } from '@/site/paths';
|
||||
import {
|
||||
TagsWithMeta,
|
||||
@ -15,8 +15,7 @@ import {
|
||||
generateLocalNaivePostgresString,
|
||||
generateLocalPostgresString,
|
||||
} from '@/utility/date';
|
||||
import { convertStringToArray } from '@/utility/string';
|
||||
import clsx from 'clsx';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRef, useState } from 'react';
|
||||
import { BiImageAdd } from 'react-icons/bi';
|
||||
@ -66,6 +65,7 @@ export default function AdminAddAllUploads({
|
||||
, 100);
|
||||
}
|
||||
}}
|
||||
readOnly={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@ -81,6 +81,7 @@ export default function AdminAddAllUploads({
|
||||
setTags(tags);
|
||||
setTagErrorMessage(getValidationMessageForTags(tags) ?? '');
|
||||
}}
|
||||
readOnly={isLoading}
|
||||
error={tagErrorMessage}
|
||||
required={false}
|
||||
hideLabel
|
||||
@ -97,10 +98,8 @@ export default function AdminAddAllUploads({
|
||||
`Are you sure you want to add all ${storageUrlCount} uploads?`
|
||||
)) {
|
||||
setIsLoading(true);
|
||||
addAllUploads({
|
||||
tags: showTags && tags
|
||||
? convertStringToArray(tags) ?? []
|
||||
: [],
|
||||
addAllUploadsAction({
|
||||
tags: showTags ? tags : undefined,
|
||||
takenAtLocal: generateLocalPostgresString(),
|
||||
takenAtNaiveLocal: generateLocalNaivePostgresString(),
|
||||
})
|
||||
|
||||
@ -149,12 +149,18 @@ export default function FieldSetWithStatus({
|
||||
autoComplete="off"
|
||||
autoCapitalize={!capitalize ? 'off' : undefined}
|
||||
readOnly={readOnly || pending || loading}
|
||||
disabled={type === 'checkbox' && (
|
||||
readOnly || pending || loading
|
||||
)}
|
||||
className={clsx(
|
||||
(
|
||||
type === 'text' ||
|
||||
type === 'email' ||
|
||||
type === 'password'
|
||||
) && 'w-full',
|
||||
type === 'checkbox' && (
|
||||
readOnly || pending || loading
|
||||
) && 'opacity-50 cursor-not-allowed',
|
||||
Boolean(error) && 'error',
|
||||
)}
|
||||
/>}
|
||||
|
||||
@ -41,14 +41,19 @@ import { convertPhotoToPhotoDbInsert } from '.';
|
||||
import { runAuthenticatedAdminServerAction } from '@/auth';
|
||||
import { AI_IMAGE_QUERIES, AiImageQuery } from './ai';
|
||||
import { streamOpenAiImageQuery } from '@/services/openai';
|
||||
import { AI_TEXT_GENERATION_ENABLED, BLUR_ENABLED } from '@/site/config';
|
||||
import {
|
||||
AI_TEXT_AUTO_GENERATED_FIELDS,
|
||||
AI_TEXT_GENERATION_ENABLED,
|
||||
BLUR_ENABLED,
|
||||
} from '@/site/config';
|
||||
import { getStorageUploadUrlsNoStore } from '@/services/storage/cache';
|
||||
import { generateAiImageQueries } from './ai/server';
|
||||
|
||||
// Private actions
|
||||
|
||||
export const createPhotoAction = async (formData: FormData) =>
|
||||
runAuthenticatedAdminServerAction(async () => {
|
||||
const photo = convertFormDataToPhotoDbInsert(formData, true);
|
||||
const photo = convertFormDataToPhotoDbInsert(formData);
|
||||
|
||||
const updatedUrl = await convertUploadToPhoto(photo.url);
|
||||
|
||||
@ -60,17 +65,18 @@ export const createPhotoAction = async (formData: FormData) =>
|
||||
}
|
||||
});
|
||||
|
||||
export const addAllUploads = async ({
|
||||
export const addAllUploadsAction = async ({
|
||||
tags,
|
||||
takenAtLocal,
|
||||
takenAtNaiveLocal,
|
||||
}: {
|
||||
tags: string[]
|
||||
tags?: string
|
||||
takenAtLocal: string
|
||||
takenAtNaiveLocal: string
|
||||
}) =>
|
||||
runAuthenticatedAdminServerAction(async () => {
|
||||
const uploadUrls = await getStorageUploadUrlsNoStore();
|
||||
|
||||
for (const { url } of uploadUrls) {
|
||||
const {
|
||||
photoFormExif,
|
||||
@ -82,23 +88,38 @@ export const addAllUploads = async ({
|
||||
});
|
||||
|
||||
if (photoFormExif) {
|
||||
const form = {
|
||||
const {
|
||||
title,
|
||||
caption,
|
||||
tags: aiTags,
|
||||
semanticDescription,
|
||||
} = await generateAiImageQueries(
|
||||
imageResizedBase64,
|
||||
AI_TEXT_AUTO_GENERATED_FIELDS,
|
||||
);
|
||||
|
||||
const form: Partial<PhotoFormData> = {
|
||||
...photoFormExif,
|
||||
tags,
|
||||
title,
|
||||
caption,
|
||||
tags: tags || aiTags,
|
||||
semanticDescription,
|
||||
takenAt: photoFormExif.takenAt || takenAtLocal,
|
||||
takenAtNaive: photoFormExif.takenAtNaive || takenAtNaiveLocal,
|
||||
};
|
||||
|
||||
const updatedUrl = await convertUploadToPhoto(url);
|
||||
if (updatedUrl) {
|
||||
const photo = convertFormDataToPhotoDbInsert(form);
|
||||
console.log(photo);
|
||||
photo.url = updatedUrl;
|
||||
await insertPhoto(photo);
|
||||
}
|
||||
}
|
||||
// const updatedUrl = await convertUploadToPhoto(url);
|
||||
// if (updatedUrl) {
|
||||
// const photo = convertFormDataToPhotoDbInsert(new FormData(), true);
|
||||
// photo.url = updatedUrl;
|
||||
// await insertPhoto(photo);
|
||||
// }
|
||||
// const photo = convertFormDataToPhotoDbInsert(new FormData(), true);
|
||||
// photo.url = url;
|
||||
// await insertPhoto(photo);
|
||||
}
|
||||
|
||||
revalidateAllKeysAndPaths();
|
||||
redirect(PATH_ADMIN_PHOTOS);
|
||||
});
|
||||
|
||||
export const updatePhotoAction = async (formData: FormData) =>
|
||||
|
||||
@ -63,3 +63,11 @@ export const parseTitleAndCaption = (text: string) => {
|
||||
caption: matches?.[2] ?? '',
|
||||
};
|
||||
};
|
||||
|
||||
export const cleanUpAiTextResponse = (text?: string) => text
|
||||
? text
|
||||
.replaceAll('\n', ' ')
|
||||
.replaceAll('"', '')
|
||||
.replace(/\.$/, '')
|
||||
.trim()
|
||||
: undefined;
|
||||
|
||||
77
src/photo/ai/server.ts
Normal file
77
src/photo/ai/server.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { generateOpenAiImageQuery } from '@/services/openai';
|
||||
import {
|
||||
AI_IMAGE_QUERIES,
|
||||
AiAutoGeneratedField,
|
||||
cleanUpAiTextResponse,
|
||||
parseTitleAndCaption,
|
||||
} from '.';
|
||||
|
||||
export const generateAiImageQueries = async (
|
||||
imageBase64?: string,
|
||||
textFieldsToGenerate: AiAutoGeneratedField[] = [],
|
||||
): Promise<{
|
||||
title?: string
|
||||
caption?: string
|
||||
tags?: string
|
||||
semanticDescription?: string
|
||||
}> => {
|
||||
let title: string | undefined;
|
||||
let caption: string | undefined;
|
||||
let tags: string | undefined;
|
||||
let semanticDescription: string | undefined;
|
||||
|
||||
if (imageBase64) {
|
||||
if (
|
||||
textFieldsToGenerate.includes('title') &&
|
||||
textFieldsToGenerate.includes('caption')
|
||||
) {
|
||||
const titleAndCaption = await generateOpenAiImageQuery(
|
||||
imageBase64,
|
||||
AI_IMAGE_QUERIES['title-and-caption'],
|
||||
);
|
||||
if (titleAndCaption) {
|
||||
const titleAndCaptionParsed = parseTitleAndCaption(titleAndCaption);
|
||||
title = titleAndCaptionParsed.title;
|
||||
caption = titleAndCaptionParsed.caption;
|
||||
}
|
||||
} else {
|
||||
if (textFieldsToGenerate.includes('title')) {
|
||||
title = cleanUpAiTextResponse(
|
||||
await generateOpenAiImageQuery(
|
||||
imageBase64,
|
||||
AI_IMAGE_QUERIES['title'],
|
||||
));
|
||||
}
|
||||
if (textFieldsToGenerate.includes('caption')) {
|
||||
caption = cleanUpAiTextResponse(
|
||||
await generateOpenAiImageQuery(
|
||||
imageBase64,
|
||||
AI_IMAGE_QUERIES['caption'],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (textFieldsToGenerate.includes('tags')) {
|
||||
tags = cleanUpAiTextResponse(
|
||||
await generateOpenAiImageQuery(
|
||||
imageBase64,
|
||||
AI_IMAGE_QUERIES['tags'],
|
||||
));
|
||||
}
|
||||
|
||||
if (textFieldsToGenerate.includes('semantic')) {
|
||||
semanticDescription = cleanUpAiTextResponse(
|
||||
await generateOpenAiImageQuery(
|
||||
imageBase64,
|
||||
AI_IMAGE_QUERIES['description-small'],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
caption,
|
||||
tags,
|
||||
semanticDescription,
|
||||
};
|
||||
};
|
||||
@ -7,7 +7,7 @@ export type AiContent = ReturnType<typeof useAiImageQueries>;
|
||||
|
||||
export default function useAiImageQueries(
|
||||
textFieldsToAutoGenerate: AiAutoGeneratedField[] = [],
|
||||
imageData?: string,
|
||||
imageBase64?: string,
|
||||
) {
|
||||
const [
|
||||
requestTitleCaption,
|
||||
@ -17,33 +17,33 @@ export default function useAiImageQueries(
|
||||
_isLoadingCaption,
|
||||
resetTitle,
|
||||
resetCaption,
|
||||
] = useTitleCaptionAiImageQuery(imageData);
|
||||
] = useTitleCaptionAiImageQuery(imageBase64);
|
||||
|
||||
const [
|
||||
requestTitle,
|
||||
titleSolo,
|
||||
isLoadingTitleSolo,
|
||||
resetTitleSolo,
|
||||
] = useAiImageQuery(imageData, 'title');
|
||||
] = useAiImageQuery(imageBase64, 'title');
|
||||
|
||||
const [
|
||||
requestCaption,
|
||||
captionSolo,
|
||||
isLoadingCaptionSolo,
|
||||
resetCaptionSolo,
|
||||
] = useAiImageQuery(imageData, 'caption');
|
||||
] = useAiImageQuery(imageBase64, 'caption');
|
||||
|
||||
const [
|
||||
requestTags,
|
||||
tags,
|
||||
isLoadingTags,
|
||||
] = useAiImageQuery(imageData, 'tags');
|
||||
] = useAiImageQuery(imageBase64, 'tags');
|
||||
|
||||
const [
|
||||
requestSemantic,
|
||||
semanticDescription,
|
||||
isLoadingSemantic,
|
||||
] = useAiImageQuery(imageData, 'description-small');
|
||||
] = useAiImageQuery(imageBase64, 'description-small');
|
||||
|
||||
const title = _title || titleSolo;
|
||||
const caption = _caption || captionSolo;
|
||||
@ -99,12 +99,12 @@ export default function useAiImageQueries(
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (imageData && !hasRunAllQueriesOnce.current) {
|
||||
if (imageBase64 && !hasRunAllQueriesOnce.current) {
|
||||
if (textFieldsToAutoGenerate.length > 0) {
|
||||
request(textFieldsToAutoGenerate);
|
||||
}
|
||||
}
|
||||
}, [textFieldsToAutoGenerate, imageData, request]);
|
||||
}, [textFieldsToAutoGenerate, imageBase64, request]);
|
||||
|
||||
return {
|
||||
request,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { streamAiImageQueryAction } from '../actions';
|
||||
import { readStreamableValue } from 'ai/rsc';
|
||||
import { AiImageQuery } from '.';
|
||||
import { AiImageQuery, cleanUpAiTextResponse } from '.';
|
||||
|
||||
export default function useAiImageQuery(
|
||||
imageBase64: string | undefined,
|
||||
@ -21,10 +21,9 @@ export default function useAiImageQuery(
|
||||
query,
|
||||
);
|
||||
for await (const text of readStreamableValue(textStream)) {
|
||||
setText(current => `${current}${text ?? ''}`
|
||||
.replaceAll('\n', ' ')
|
||||
.replaceAll('"', '')
|
||||
.replace(/\.$/, ''));
|
||||
setText(current =>
|
||||
cleanUpAiTextResponse(`${current}${text ?? ''}`) ?? ''
|
||||
);
|
||||
}
|
||||
setIsLoading(false);
|
||||
} catch (e) {
|
||||
|
||||
@ -3,7 +3,7 @@ import useAiImageQuery from './useAiImageQuery';
|
||||
import { parseTitleAndCaption } from '.';
|
||||
|
||||
export default function useTitleCaptionAiImageQuery(
|
||||
imageBase64: string | undefined,
|
||||
imageBase64?: string,
|
||||
) {
|
||||
const [
|
||||
request,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { ExifData } from 'ts-exif-parser';
|
||||
import { Photo, PhotoDbInsert, PhotoExif } from '..';
|
||||
import { DEFAULT_ASPECT_RATIO, Photo, PhotoDbInsert, PhotoExif } from '..';
|
||||
import {
|
||||
convertTimestampToNaivePostgresString,
|
||||
convertTimestampWithOffsetToPostgresString,
|
||||
@ -20,7 +20,7 @@ import { TAG_FAVS, getValidationMessageForTags } from '@/tag';
|
||||
|
||||
type VirtualFields = 'favorite';
|
||||
|
||||
export type PhotoFormData = Record<keyof PhotoDbInsert | VirtualFields, string>;
|
||||
export type PhotoFormData = Record<keyof PhotoDbInsert | VirtualFields, string>
|
||||
|
||||
export type FieldSetType =
|
||||
'text' |
|
||||
@ -217,8 +217,7 @@ export const convertExifToFormData = (
|
||||
// PREPARE FORM FOR DB INSERT
|
||||
|
||||
export const convertFormDataToPhotoDbInsert = (
|
||||
formData: FormData | PhotoFormData,
|
||||
generateId?: boolean,
|
||||
formData: FormData | Partial<PhotoFormData>,
|
||||
): PhotoDbInsert => {
|
||||
const photoForm = formData instanceof FormData
|
||||
? Object.fromEntries(formData) as PhotoFormData
|
||||
@ -245,11 +244,13 @@ export const convertFormDataToPhotoDbInsert = (
|
||||
|
||||
return {
|
||||
...(photoForm as PhotoFormData & { filmSimulation?: FilmSimulation }),
|
||||
...(generateId && !photoForm.id) && { id: generateNanoid() },
|
||||
...!photoForm.id && { id: generateNanoid() },
|
||||
// Convert form strings to arrays
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
// Convert form strings to numbers
|
||||
aspectRatio: roundToNumber(parseFloat(photoForm.aspectRatio), 6),
|
||||
aspectRatio: photoForm.aspectRatio
|
||||
? roundToNumber(parseFloat(photoForm.aspectRatio), 6)
|
||||
: DEFAULT_ASPECT_RATIO,
|
||||
focalLength: photoForm.focalLength
|
||||
? parseInt(photoForm.focalLength)
|
||||
: undefined,
|
||||
|
||||
@ -31,6 +31,8 @@ export const INFINITE_SCROLL_GRID_PHOTO_MULTIPLE = HIGH_DENSITY_GRID
|
||||
// Thumbnails below /p/[photoId]
|
||||
export const RELATED_GRID_PHOTOS_TO_SHOW = 12;
|
||||
|
||||
export const DEFAULT_ASPECT_RATIO = 1.5;
|
||||
|
||||
export const ACCEPTED_PHOTO_FILE_TYPES = [
|
||||
'image/jpg',
|
||||
'image/jpeg',
|
||||
|
||||
@ -101,6 +101,6 @@ export const generateOpenAiImageQuery = async (
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
}).then(({ text }) => text);
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user