Asset uploads allow users to submit image and voice files that feed into the Indexing Pipeline. Uploaded assets go through the Media System (which stores S3 coordinates and serves content via a proxy), are recorded in the likeness_assets table, and automatically queued for AI tag generation.
Both image and voice uploads follow the same architecture: an API route delegates to a service function in @zooly/likeness-search, which uses uploadMediaFile + createMedia, stores the generated filename (not raw S3 URLs) as contentUrl, and enqueues indexing events.
Endpoint: POST /api/ui-api/user-image-assets/add
Location: apps/zooly-app/app/api/ui-api/user-image-assets/add/route.ts
Authentication: Required (user cookie)
Service Function: addImageAssets() from @zooly/likeness-search
Supports two input formats:
POST /api/ui-api/user-image-assets/add
Content-Type: multipart/form-data
FormData:
files: File[] (one or more image files)
Accepted MIME types: image/jpeg, image/png, image/webp
{
"imageUrls": [
"https://example.com/photo1.jpg",
"https://example.com/photo2.png"
]
}
{
"success": true,
"message": "Processed 2 URLs",
"created": 2,
"duplicates": 0,
"assetIds": ["abc123...", "def456..."]
}
Status Codes:
200 - Success (all or partial)400 - Bad request (no files/URLs provided, all invalid)401 - Unauthorized500 - Internal server errorEndpoint: POST /api/ui-api/user-voice-assets/add
Location: apps/zooly-app/app/api/ui-api/user-voice-assets/add/route.ts
Authentication: Required (user cookie)
Service Function: addVoiceAssets() from @zooly/likeness-search
Supports two input formats:
POST /api/ui-api/user-voice-assets/add
Content-Type: multipart/form-data
FormData:
files: File[] (one or more audio files)
Accepted file types: Audio files (audio/*, video/*)
{
"voiceUrls": [
"https://example.com/audio1.mp3",
"https://example.com/audio2.wav"
]
}
Video platform URLs (YouTube, Vimeo, TikTok, etc.) are rejected. Only direct audio file URLs are accepted.
{
"success": true,
"message": "Processed 2 URLs",
"created": 2,
"duplicates": 0,
"assetIds": ["abc123...", "def456..."]
}
Status Codes:
200 - Success (all or partial)400 - Bad request (no files/URLs provided, all invalid)401 - Unauthorized500 - Internal server erroraddImageAssets(cookieHeader, imageUrls?, files?)Location: packages/likeness-search/src/addImageAssets.ts
Import: import { addImageAssets } from "@zooly/likeness-search"
Process:
accountId from cookie via resolveAccountId()uploadMediaFile(buffer, file.name, file.type, "likeness-image-assets"), then createMedia() to persist S3 metadatalikeness_assets records (type IMAGE) for duplicate contentUrl valueslikeness_assets records with type: "IMAGE" and contentUrl set to the generated filename (not S3 URL)IMAGE_ASSET event via addToQueue() to trigger the indexing pipelineSee Media System for details on how filenames are generated and served via the proxy.
addVoiceAssets(cookieHeader, voiceUrls?, files?)Location: packages/likeness-search/src/addVoiceAssets.ts
Import: import { addVoiceAssets } from "@zooly/likeness-search"
Process:
accountId from cookie via resolveAccountId()audio/* or video/*) and uploads each via uploadMediaFile(buffer, file.name, file.type, "likeness-voice-assets"), then createMedia() to persist S3 metadatavalidateAudioUrl() (rejects video platform URLs)likeness_assets records (type VOICE) for duplicate contentUrl valueslikeness_assets records with type: "VOICE" and contentUrl set to the generated filenameVOICE_ASSET event via addToQueue() to trigger the indexing pipelineSee Media System for details.
Assets are stored in the project's S3 bucket with the following key structure:
| Asset Type | S3 Key Pattern | Example |
|---|---|---|
| Image | likeness-image-assets/{filename} | likeness-image-assets/photo-a3f2.jpg |
| Voice | likeness-voice-assets/{filename} | likeness-voice-assets/sample-x7k9.mp3 |
contentUrl in likeness_assets stores the filename (e.g., photo-a3f2.jpg), not the raw S3 URL. Clients receive proxy URLs via getMediaUrl(filename) — e.g., {NEXT_PUBLIC_APP_URL}/api/media/photo-a3f2.jpg. The Media System documents the proxy endpoint, media table, and upload helpers.
Each upload creates a record in the likeness_assets table:
{
id: nanoid(), // auto-generated
accountId: "...", // from authenticated user
type: "IMAGE" | "VOICE",
contentUrl: "photo-a3f2.jpg", // media filename (not S3 URL)
description: null,
searchTags: null, // populated later by AI tag generation
voiceSampleUrl: null, // populated later for VOICE assets (also filename)
tagAttemptCount: 0,
tagLastAttemptAt: null,
}
S3 metadata (key, bucket, region) is stored in the media table. List endpoints and search results map filenames through getMediaUrl() before returning to clients.
For schema details, see Database Schema — likeness_assets.
Both upload functions check for existing assets before creating new records:
likeness_assets for the account filtered by type (IMAGE or VOICE)contentUrl valuesThis prevents the same file from being uploaded and indexed multiple times.
After assets are created, the upload functions enqueue events that trigger the Indexing Pipeline:
| Upload Type | Queue Event | What Happens Next |
|---|---|---|
| Image | IMAGE_ASSET | Cron daemon picks up event → data sufficiency check → AI tag generation via Gemini vision → upsert to search index |
| Voice | VOICE_ASSET | Cron daemon picks up event → data sufficiency check → AI tag generation via Gemini audio + ElevenLabs voice clone → upsert to search index |
The indexing daemon runs every 2 minutes. After an upload, the asset will typically be processed and searchable within one or two cron cycles, depending on queue depth.
Assets are uploaded with searchTags: null. The AI tag generation sub-process (triggered by the queue event) fills in searchTags using Gemini vision/audio analysis. For voice assets, a voice sample is also created via ElevenLabs. See Indexing Pipeline — Sub-Processes for details.
likeness_assets and media table definitionsOn This Page
OverviewArchitectureAPI EndpointsAdd Image AssetsFile Upload (Multipart Form Data)URL-Based Upload (JSON)ResponseAdd Voice AssetsFile Upload (Multipart Form Data)URL-Based Upload (JSON)ResponseService Functions[object Object][object Object]S3 Storage and Media SystemDatabase RecordsDeduplicationIndexing Pipeline IntegrationRelated Documentation