Document AI text generation features

This commit is contained in:
Sam Becker 2024-03-20 15:31:28 -05:00
parent a351999e37
commit f7aa65101d
9 changed files with 66 additions and 14 deletions

View File

@ -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
<img src="/readme/og-image-share.png" alt="OG Image Preview" width=600 />
@ -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

View File

@ -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 (
<PhotoEditPageClient {...{ photo, uniqueTags }} />
<PhotoEditPageClient {...{
photo,
uniqueTags,
aiTextGeneration,
}} />
);
};

View File

@ -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 (
<UploadPageClient {...{ blobId, photoFormExif, uniqueTags }} />
<UploadPageClient {...{
blobId,
photoFormExif,
uniqueTags,
aiTextGeneration,
}} />
);
};

View File

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

View File

@ -11,10 +11,12 @@ export default function UploadPageClient({
blobId,
photoFormExif,
uniqueTags,
aiTextGeneration,
}: {
blobId?: string
photoFormExif: Partial<PhotoFormData>
uniqueTags: Tags
aiTextGeneration: boolean
}) {
const [pending, setIsPending] = useState(false);
const [updatedTitle, setUpdatedTitle] = useState('');
@ -31,6 +33,7 @@ export default function UploadPageClient({
<PhotoForm
initialPhotoForm={photoFormExif}
uniqueTags={uniqueTags}
aiTextGeneration={aiTextGeneration}
onTitleChange={setUpdatedTitle}
onFormStatusChange={setIsPending}
/>

View File

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

View File

@ -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<keyof PhotoFormData, FormMeta> => ({
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',

View File

@ -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,
) =>
<div
key={variable}
className="overflow-x-scroll overflow-y-hidden"
className={clsx(
'overflow-x-scroll overflow-y-hidden',
minimal && 'inline-flex',
)}
>
<span className="inline-flex items-center gap-1">
<span className={clsx(
@ -105,13 +112,13 @@ export default function SiteChecklistClient({
)}>
`{variable}`
</span>
{renderCopyButton(variable, variable, true)}
{!minimal && renderCopyButton(variable, variable, true)}
</span>
</div>;
const renderEnvVars = (variables: string[]) =>
<div className="py-1 space-y-1">
{variables.map(renderEnvVar)}
{variables.map(envVar => renderEnvVar(envVar))}
</div>;
const renderSubStatus = (
@ -294,6 +301,17 @@ export default function SiteChecklistClient({
collection/display of location-based data
{renderEnvVars(['NEXT_PUBLIC_GEO_PRIVACY'])}
</ChecklistRow>
<ChecklistRow
title="AI-generated Text"
status={isAiTextGenerationEnabled}
isPending={isPendingPage}
optional
>
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'])}
</ChecklistRow>
<ChecklistRow
title="Priority Order"
status={isPriorityOrderEnabled}

View File

@ -84,6 +84,8 @@ export const CURRENT_STORAGE: StorageType =
export const PRO_MODE_ENABLED = process.env.NEXT_PUBLIC_PRO_MODE === '1';
export const BLUR_ENABLED = process.env.NEXT_PUBLIC_BLUR_DISABLED !== '1';
export const GEO_PRIVACY_ENABLED = process.env.NEXT_PUBLIC_GEO_PRIVACY === '1';
export const AI_TEXT_GENERATION_ENABLED =
Boolean(process.env.OPENAI_SECRET_KEY);
export const PRIORITY_ORDER_ENABLED =
process.env.NEXT_PUBLIC_IGNORE_PRIORITY_ORDER !== '1';
export const PUBLIC_API_ENABLED = process.env.NEXT_PUBLIC_PUBLIC_API === '1';
@ -123,6 +125,7 @@ export const CONFIG_CHECKLIST_STATUS = {
isProModeEnabled: PRO_MODE_ENABLED,
isBlurEnabled: BLUR_ENABLED,
isGeoPrivacyEnabled: GEO_PRIVACY_ENABLED,
isAiTextGenerationEnabled: AI_TEXT_GENERATION_ENABLED,
isPriorityOrderEnabled: PRIORITY_ORDER_ENABLED,
isPublicApiEnabled: PUBLIC_API_ENABLED,
isOgTextBottomAligned: OG_TEXT_BOTTOM_ALIGNMENT,