diff --git a/src/admin/AdminAddAllUploads.tsx b/src/admin/AdminAddAllUploads.tsx
index 223e9e4f..cc8f452a 100644
--- a/src/admin/AdminAddAllUploads.tsx
+++ b/src/admin/AdminAddAllUploads.tsx
@@ -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}
/>
}
diff --git a/src/photo/actions.ts b/src/photo/actions.ts
index 71ef0bbf..887da0f1 100644
--- a/src/photo/actions.ts
+++ b/src/photo/actions.ts
@@ -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 = {
...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) =>
diff --git a/src/photo/ai/index.ts b/src/photo/ai/index.ts
index 4fbb228e..91db8318 100644
--- a/src/photo/ai/index.ts
+++ b/src/photo/ai/index.ts
@@ -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;
diff --git a/src/photo/ai/server.ts b/src/photo/ai/server.ts
new file mode 100644
index 00000000..367c89c0
--- /dev/null
+++ b/src/photo/ai/server.ts
@@ -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,
+ };
+};
diff --git a/src/photo/ai/useAiImageQueries.ts b/src/photo/ai/useAiImageQueries.ts
index 8c78ab7c..5ed5af29 100644
--- a/src/photo/ai/useAiImageQueries.ts
+++ b/src/photo/ai/useAiImageQueries.ts
@@ -7,7 +7,7 @@ export type AiContent = ReturnType;
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,
diff --git a/src/photo/ai/useAiImageQuery.ts b/src/photo/ai/useAiImageQuery.ts
index 259d8c5c..f75c7395 100644
--- a/src/photo/ai/useAiImageQuery.ts
+++ b/src/photo/ai/useAiImageQuery.ts
@@ -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) {
diff --git a/src/photo/ai/useTitleCaptionAiImageQuery.ts b/src/photo/ai/useTitleCaptionAiImageQuery.ts
index 6768decd..f99483ec 100644
--- a/src/photo/ai/useTitleCaptionAiImageQuery.ts
+++ b/src/photo/ai/useTitleCaptionAiImageQuery.ts
@@ -3,7 +3,7 @@ import useAiImageQuery from './useAiImageQuery';
import { parseTitleAndCaption } from '.';
export default function useTitleCaptionAiImageQuery(
- imageBase64: string | undefined,
+ imageBase64?: string,
) {
const [
request,
diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts
index 3ad6bb27..da306ab0 100644
--- a/src/photo/form/index.ts
+++ b/src/photo/form/index.ts
@@ -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;
+export type PhotoFormData = Record
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,
): 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,
diff --git a/src/photo/index.ts b/src/photo/index.ts
index 01b0d0b0..0390ad58 100644
--- a/src/photo/index.ts
+++ b/src/photo/index.ts
@@ -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',
diff --git a/src/services/openai.ts b/src/services/openai.ts
index f8ff29f1..4eda3990 100644
--- a/src/services/openai.ts
+++ b/src/services/openai.ts
@@ -101,6 +101,6 @@ export const generateOpenAiImageQuery = async (
},
],
}],
- });
+ }).then(({ text }) => text);
}
};