Document AI text generation features
This commit is contained in:
parent
a351999e37
commit
f7aa65101d
@ -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
|
||||
|
||||
@ -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,
|
||||
}} />
|
||||
);
|
||||
};
|
||||
|
||||
@ -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,
|
||||
}} />
|
||||
);
|
||||
};
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user