diff --git a/README.md b/README.md index 1a462320..456ddc6d 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,14 @@ https://photos.sambecker.com Features - +- Built-in auth - Photo upload with EXIF extraction - Organize photos by tag and camera model - Infinite scroll -- Built-in auth - Light/dark mode +- CMD-K menu with photo search - Automatic OG image generation +- Experimental support for AI-generated descriptions - Support for Fujifilm simulations OG Image Preview @@ -71,6 +73,7 @@ Installation - `NEXT_PUBLIC_PRO_MODE = 1` enables higher quality image storage for jpgs (will result in increased storage usage) - `NEXT_PUBLIC_BLUR_DISABLED = 1` prevents image blur data being stored and displayed (potentially useful for limiting Postgres usage) - `NEXT_PUBLIC_GEO_PRIVACY = 1` disables collection/display of location-based data +- `OPENAI_SECRET_KEY = [Your Key]` enables experimental support for AI-generated text descriptions - `NEXT_PUBLIC_IGNORE_PRIORITY_ORDER = 1` prevents `priority_order` field affecting photo order - `NEXT_PUBLIC_PUBLIC_API = 1` enables public API available at `/api` - `NEXT_PUBLIC_HIDE_REPO_LINK = 1` removes footer link to repo diff --git a/src/app/admin/photos/[photoId]/edit/page.tsx b/src/app/admin/photos/[photoId]/edit/page.tsx index 2a5c2d6c..767ba55b 100644 --- a/src/app/admin/photos/[photoId]/edit/page.tsx +++ b/src/app/admin/photos/[photoId]/edit/page.tsx @@ -2,6 +2,7 @@ import { redirect } from 'next/navigation'; import { getPhotoNoStore, getUniqueTagsCached } from '@/photo/cache'; import { PATH_ADMIN } from '@/site/paths'; import PhotoEditPageClient from '@/photo/PhotoEditPageClient'; +import { AI_TEXT_GENERATION_ENABLED } from '@/site/config'; export default async function PhotoEditPage({ params: { photoId }, @@ -14,7 +15,13 @@ export default async function PhotoEditPage({ const uniqueTags = await getUniqueTagsCached(); + const aiTextGeneration = AI_TEXT_GENERATION_ENABLED; + return ( - + ); }; diff --git a/src/app/admin/uploads/[uploadPath]/page.tsx b/src/app/admin/uploads/[uploadPath]/page.tsx index b59ccc45..d075631c 100644 --- a/src/app/admin/uploads/[uploadPath]/page.tsx +++ b/src/app/admin/uploads/[uploadPath]/page.tsx @@ -3,6 +3,7 @@ import { extractExifDataFromBlobPath } from '@/photo/server'; import { redirect } from 'next/navigation'; import { getUniqueTagsCached } from '@/photo/cache'; import UploadPageClient from '@/photo/UploadPageClient'; +import { AI_TEXT_GENERATION_ENABLED } from '@/site/config'; interface Params { params: { uploadPath: string } @@ -14,11 +15,18 @@ export default async function UploadPage({ params: { uploadPath } }: Params) { photoFormExif, } = await extractExifDataFromBlobPath(uploadPath); - const uniqueTags = await getUniqueTagsCached(); - if (!photoFormExif) { redirect(PATH_ADMIN); } + const uniqueTags = await getUniqueTagsCached(); + + const aiTextGeneration = AI_TEXT_GENERATION_ENABLED; + return ( - + ); }; diff --git a/src/photo/PhotoEditPageClient.tsx b/src/photo/PhotoEditPageClient.tsx index 6be696f5..a26c054d 100644 --- a/src/photo/PhotoEditPageClient.tsx +++ b/src/photo/PhotoEditPageClient.tsx @@ -16,9 +16,11 @@ import { useState } from 'react'; export default function PhotoEditPageClient({ photo, uniqueTags, + aiTextGeneration, }: { photo: Photo - uniqueTags?: Tags + uniqueTags: Tags + aiTextGeneration: boolean }) { const seedExifData = { url: photo.url }; @@ -62,6 +64,7 @@ export default function PhotoEditPageClient({ ? updatedExifData : undefined} uniqueTags={uniqueTags} + aiTextGeneration={aiTextGeneration} onTitleChange={setUpdatedTitle} onFormStatusChange={setIsPending} /> diff --git a/src/photo/UploadPageClient.tsx b/src/photo/UploadPageClient.tsx index b37a4d39..be755510 100644 --- a/src/photo/UploadPageClient.tsx +++ b/src/photo/UploadPageClient.tsx @@ -11,10 +11,12 @@ export default function UploadPageClient({ blobId, photoFormExif, uniqueTags, + aiTextGeneration, }: { blobId?: string photoFormExif: Partial uniqueTags: Tags + aiTextGeneration: boolean }) { const [pending, setIsPending] = useState(false); const [updatedTitle, setUpdatedTitle] = useState(''); @@ -31,6 +33,7 @@ export default function UploadPageClient({ diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index e7068c69..0ff11a6e 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -35,6 +35,7 @@ export default function PhotoForm({ updatedExifData, type = 'create', uniqueTags, + aiTextGeneration, debugBlur, onTitleChange, onFormStatusChange, @@ -43,6 +44,7 @@ export default function PhotoForm({ updatedExifData?: Partial type?: 'create' | 'edit' uniqueTags?: Tags + aiTextGeneration?: boolean debugBlur?: boolean onTitleChange?: (updatedTitle: string) => void onFormStatusChange?: (pending: boolean) => void @@ -322,7 +324,8 @@ export default function PhotoForm({ value: tag, annotation: formatCount(count), annotationAria: formatCountDescriptive(count, 'tagged'), - })) + })), + aiTextGeneration, ) .map(([key, { label, diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts index 078e36c6..39ec8661 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -13,7 +13,10 @@ import { MAKE_FUJIFILM, } from '@/vendors/fujifilm'; import { FilmSimulation } from '@/simulation'; -import { BLUR_ENABLED, GEO_PRIVACY_ENABLED } from '@/site/config'; +import { + BLUR_ENABLED, + GEO_PRIVACY_ENABLED, +} from '@/site/config'; import { TAG_FAVS, doesTagsStringIncludeFavs } from '@/tag'; type VirtualFields = 'favorite'; @@ -55,7 +58,8 @@ const STRING_MAX_LENGTH_SHORT = 255; const STRING_MAX_LENGTH_LONG = 1000; const FORM_METADATA = ( - tagOptions?: AnnotatedTag[] + tagOptions?: AnnotatedTag[], + aiTextGeneration?: boolean, ): Record => ({ title: { label: 'title', @@ -72,7 +76,7 @@ const FORM_METADATA = ( label: 'semantic description', capitalize: true, validateStringMaxLength: STRING_MAX_LENGTH_LONG, - hide: true, + hide: !aiTextGeneration, }, tags: { label: 'tags', diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx index 7b08db87..0d8febec 100644 --- a/src/site/SiteChecklistClient.tsx +++ b/src/site/SiteChecklistClient.tsx @@ -40,6 +40,7 @@ export default function SiteChecklistClient({ isBlurEnabled, isGeoPrivacyEnabled, isPriorityOrderEnabled, + isAiTextGenerationEnabled, isPublicApiEnabled, isOgTextBottomAligned, gridAspectRatio, @@ -92,10 +93,16 @@ export default function SiteChecklistClient({ }} />; - const renderEnvVar = (variable: string) => + const renderEnvVar = ( + variable: string, + minimal?: boolean, + ) =>
`{variable}` - {renderCopyButton(variable, variable, true)} + {!minimal && renderCopyButton(variable, variable, true)}
; const renderEnvVars = (variables: string[]) =>
- {variables.map(renderEnvVar)} + {variables.map(envVar => renderEnvVar(envVar))}
; const renderSubStatus = ( @@ -294,6 +301,17 @@ export default function SiteChecklistClient({ collection/display of location-based data {renderEnvVars(['NEXT_PUBLIC_GEO_PRIVACY'])} + + Store your OpenAI secret key in order to add experimental support + for AI-generated text descriptions and enable an invisible field + called {'"Semantic Description"'} used to support CMD-K search + {renderEnvVars(['OPENAI_SECRET_KEY'])} +