mirror of
https://github.com/transloadit/uppy.git
synced 2026-01-23 02:25:07 +00:00
Closes #5378
- Introduce `@uppy/image-generator`, a new plugin to generate images
based on a prompt via Transloadit
- until we have "golden templates" the idea is to just send
[steps](https://transloadit.com/docs/topics/templates/#overruling-templates-at-runtime)
- because we must send steps and since we must use signature
authentication for security, which is signed based on the params we
send, we can't reuse the `assemblyOptions` the consumers is already
passing to `@uppy/transloadit` (if they use that uploaders, not needed).
- Remove `SearchInput` (this component was trying to be too many things,
all with conditional boolean props, which is bad practise) in favor of
`useSearchForm` and reuse this hook in two new components `SearchView`
and `FilterInput`
- Reuse all the styles from `SearchProviderView`. This deviates from the
design in #5378. It felt too inconsistent to me to do another UI here
again. For the initial version, I think it's best to stay consistent and
then redesign with search providers taken into account too.
- Because the service is so slow, I went a bit further with the loading
state to show funny messages that rotate while loading mostly because
users will start thinking it is broken after 5 seconds while it fact we
are still loading. But open to ideas here.
This unfortunately means the integration for the consumer is not as lean
and pretty as you would hope. On the upside, it does give them complete
freedom.
```ts
.use(ImageGenerator, {
assemblyOptions: async (prompt) => {
const res = await fetch(`/assembly-options?prompt=${encodeURIComponent(prompt)}`)
return res.json()
}
})
```
on the consumer's server:
```ts
import crypto from 'node:crypto'
const utcDateString = (ms) => {
return new Date(ms)
.toISOString()
.replace(/-/g, '/')
.replace(/T/, ' ')
.replace(/\.\d+Z$/, '+00:00')
}
// expire 1 hour from now (this must be milliseconds)
const expires = utcDateString(Date.now() + 1 * 60 * 60 * 1000)
const authKey = 'YOUR_TRANSLOADIT_KEY'
const authSecret = 'YOUR_TRANSLOADIT_SECRET'
const params = JSON.stringify({
auth: {
key: authKey,
expires,
},
// can not contain any more steps, the only step must be /image/generate
steps: {
generated_image: { // can be named different
robot: '/image/generate',
result: true, // mandatory
aspect_ratio: '2:3', // up to them
model: 'flux-1.1-pro-ultra', // up to them
prompt, // mandatory
num_outputs: 2, // up to them
},
},
})
const signatureBytes = crypto.createHmac('sha384', authSecret).update(Buffer.from(params, 'utf-8'))
// The final signature needs the hash name in front, so
// the hashing algorithm can be updated in a backwards-compatible
// way when old algorithms become insecure.
const signature = `sha384:${signatureBytes.digest('hex')}`
// respond with { params, signature } JSON to the client
```
https://github.com/user-attachments/assets/9217e457-b38b-48ac-81f0-37a417309e98
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Adds AI image generation plugin using Transloadit, exports low-level
Transloadit APIs, and replaces SearchInput with new
FilterInput/SearchView + useSearchForm across provider views.
>
> - **New plugin: `@uppy/image-generator`**
> - UI plugin to generate images from a prompt via Transloadit
(`src/index.tsx`, styles, locale, build configs).
> - Integrated into dev Dashboard and included in `uppy` bundle and
global styles.
> - **Provider Views refactor**
> - Remove `SearchInput`; introduce `useSearchForm`, `SearchView`, and
`FilterInput` components.
> - Update `ProviderView`, `SearchProviderView`, and `Webdav` to use new
components; export them from `@uppy/provider-views`.
> - **Transloadit updates**
> - Export `Assembly`, `AssemblyError`, and `Client` from
`@uppy/transloadit`.
> - Minor internal change: normalize `assemblyOptions.fields`.
> - **Locales**
> - Add strings for image generation and minor additions (e.g.,
`chooseFiles`).
> - Ensure locales build depends on `@uppy/image-generator`.
> - **Build config**
> - Turborepo: add `uppy#build:css` and hook `image-generator` into
locales build.
> - **Changesets**
> - `@uppy/image-generator` major; `@uppy/transloadit` minor;
`@uppy/locales` and `uppy` minor; `@uppy/provider-views` and
`@uppy/webdav` patch.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
4b1b729069. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Prakash <qxprakash@gmail.com>
281 lines
8.4 KiB
JavaScript
281 lines
8.4 KiB
JavaScript
import Audio from '@uppy/audio'
|
|
import AwsS3 from '@uppy/aws-s3'
|
|
import Compressor from '@uppy/compressor'
|
|
import Uppy, { debugLogger } from '@uppy/core'
|
|
import Dashboard from '@uppy/dashboard'
|
|
import DropTarget from '@uppy/drop-target'
|
|
import Form from '@uppy/form'
|
|
import GoldenRetriever from '@uppy/golden-retriever'
|
|
import GoogleDrive from '@uppy/google-drive'
|
|
import GoogleDrivePicker from '@uppy/google-drive-picker'
|
|
import GooglePhotosPicker from '@uppy/google-photos-picker'
|
|
import ImageEditor from '@uppy/image-editor'
|
|
import ImageGenerator from '@uppy/image-generator'
|
|
import english from '@uppy/locales/lib/en_US.js'
|
|
import RemoteSources from '@uppy/remote-sources'
|
|
import ScreenCapture from '@uppy/screen-capture'
|
|
import Transloadit from '@uppy/transloadit'
|
|
import Tus from '@uppy/tus'
|
|
import Webcam from '@uppy/webcam'
|
|
import Webdav from '@uppy/webdav'
|
|
import XHRUpload from '@uppy/xhr-upload'
|
|
|
|
import generateSignatureIfSecret from './generateSignatureIfSecret.js'
|
|
|
|
// DEV CONFIG: create a .env file in the project root directory to customize those values.
|
|
const {
|
|
VITE_UPLOADER: UPLOADER,
|
|
VITE_COMPANION_URL: COMPANION_URL,
|
|
VITE_TUS_ENDPOINT: TUS_ENDPOINT,
|
|
VITE_XHR_ENDPOINT: XHR_ENDPOINT,
|
|
VITE_TRANSLOADIT_KEY: TRANSLOADIT_KEY,
|
|
VITE_TRANSLOADIT_SECRET: TRANSLOADIT_SECRET,
|
|
VITE_TRANSLOADIT_TEMPLATE: TRANSLOADIT_TEMPLATE,
|
|
VITE_TRANSLOADIT_SERVICE_URL: TRANSLOADIT_SERVICE_URL,
|
|
VITE_GOOGLE_PICKER_API_KEY: GOOGLE_PICKER_API_KEY,
|
|
VITE_GOOGLE_PICKER_CLIENT_ID: GOOGLE_PICKER_CLIENT_ID,
|
|
VITE_GOOGLE_PICKER_APP_ID: GOOGLE_PICKER_APP_ID,
|
|
} = import.meta.env
|
|
|
|
const companionAllowedHosts =
|
|
import.meta.env.VITE_COMPANION_ALLOWED_HOSTS &&
|
|
new RegExp(import.meta.env.VITE_COMPANION_ALLOWED_HOSTS)
|
|
|
|
import.meta.env.VITE_TRANSLOADIT_KEY &&= '***' // to avoid leaking secrets in screenshots.
|
|
import.meta.env.VITE_TRANSLOADIT_SECRET &&= '***' // to avoid leaking secrets in screenshots.
|
|
console.log(import.meta.env)
|
|
|
|
// DEV CONFIG: enable or disable Golden Retriever
|
|
|
|
const RESTORE = false
|
|
const COMPRESS = false
|
|
|
|
async function assemblyOptions() {
|
|
return generateSignatureIfSecret(TRANSLOADIT_SECRET, {
|
|
auth: {
|
|
key: TRANSLOADIT_KEY,
|
|
},
|
|
// It's more secure to use a template_id and enable
|
|
// Signature Authentication
|
|
template_id: TRANSLOADIT_TEMPLATE,
|
|
})
|
|
}
|
|
|
|
function getCompanionKeysParams(name) {
|
|
const {
|
|
[`VITE_COMPANION_${name}_KEYS_PARAMS_CREDENTIALS_NAME`]: credentialsName,
|
|
[`VITE_COMPANION_${name}_KEYS_PARAMS_KEY`]: key,
|
|
} = import.meta.env
|
|
|
|
if (credentialsName && key) {
|
|
// https://github.com/transloadit/uppy/pull/2802#issuecomment-1023093616
|
|
return {
|
|
companionKeysParams: {
|
|
key,
|
|
credentialsName,
|
|
},
|
|
}
|
|
}
|
|
|
|
return {}
|
|
}
|
|
|
|
// Rest is implementation! Obviously edit as necessary...
|
|
|
|
export default () => {
|
|
const restrictions = undefined
|
|
// const restrictions = {
|
|
// maxFileSize: 1 * 1000000, // 1mb
|
|
// minFileSize: 1 * 1000000, // 1mb
|
|
// maxTotalFileSize: 1 * 1000000, // 1mb
|
|
// maxNumberOfFiles: 3,
|
|
// minNumberOfFiles: 1,
|
|
// allowedFileTypes: ['image/*', '.jpg', '.jpeg', '.png', '.gif'],
|
|
// requiredMetaFields: ['caption'],
|
|
// }
|
|
|
|
const uppyDashboard = new Uppy({
|
|
locale: english,
|
|
logger: debugLogger,
|
|
meta: {
|
|
username: 'John',
|
|
license: 'Creative Commons',
|
|
},
|
|
allowMultipleUploadBatches: false,
|
|
restrictions,
|
|
})
|
|
.use(Dashboard, {
|
|
trigger: '#pick-files',
|
|
// inline: true,
|
|
target: '.foo',
|
|
metaFields: [
|
|
{ id: 'license', name: 'License', placeholder: 'specify license' },
|
|
{ id: 'caption', name: 'Caption', placeholder: 'add caption' },
|
|
],
|
|
hideProgressDetails: true,
|
|
proudlyDisplayPoweredByUppy: true,
|
|
note: `${JSON.stringify(restrictions)}`,
|
|
})
|
|
.use(GoogleDrive, {
|
|
target: Dashboard,
|
|
companionUrl: COMPANION_URL,
|
|
companionAllowedHosts,
|
|
...getCompanionKeysParams('GOOGLE_DRIVE'),
|
|
})
|
|
// .use(Instagram, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts })
|
|
// .use(Dropbox, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts })
|
|
// .use(Box, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts })
|
|
// .use(Facebook, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts })
|
|
// .use(OneDrive, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts })
|
|
// .use(Zoom, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts })
|
|
// .use(Url, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts })
|
|
// .use(Unsplash, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts })
|
|
.use(GoogleDrivePicker, {
|
|
target: Dashboard,
|
|
companionUrl: COMPANION_URL,
|
|
companionAllowedHosts,
|
|
clientId: GOOGLE_PICKER_CLIENT_ID,
|
|
apiKey: GOOGLE_PICKER_API_KEY,
|
|
appId: GOOGLE_PICKER_APP_ID,
|
|
})
|
|
.use(GooglePhotosPicker, {
|
|
target: Dashboard,
|
|
companionUrl: COMPANION_URL,
|
|
companionAllowedHosts,
|
|
clientId: GOOGLE_PICKER_CLIENT_ID,
|
|
})
|
|
.use(RemoteSources, {
|
|
companionUrl: COMPANION_URL,
|
|
sources: [
|
|
'Box',
|
|
'Dropbox',
|
|
'Facebook',
|
|
'Instagram',
|
|
'OneDrive',
|
|
'Unsplash',
|
|
'Zoom',
|
|
'Url',
|
|
],
|
|
companionAllowedHosts,
|
|
})
|
|
.use(Webcam, {
|
|
target: Dashboard,
|
|
showVideoSourceDropdown: true,
|
|
showRecordingLength: true,
|
|
})
|
|
.use(Webdav, {
|
|
target: Dashboard,
|
|
companionUrl: COMPANION_URL,
|
|
companionAllowedHosts,
|
|
})
|
|
.use(Audio, {
|
|
target: Dashboard,
|
|
showRecordingLength: true,
|
|
})
|
|
.use(ScreenCapture, { target: Dashboard })
|
|
.use(Form, { target: '#upload-form' })
|
|
.use(ImageEditor, { target: Dashboard })
|
|
.use(ImageGenerator, {
|
|
target: Dashboard,
|
|
assemblyOptions: async (prompt) =>
|
|
// never create a signature on the client in production!
|
|
// it will expose the secret on the client
|
|
generateSignatureIfSecret(TRANSLOADIT_SECRET, {
|
|
auth: { key: TRANSLOADIT_KEY },
|
|
steps: {
|
|
generated_image: {
|
|
robot: '/image/generate',
|
|
result: true,
|
|
aspect_ratio: '2:3',
|
|
model: 'flux-1.1-pro-ultra',
|
|
prompt,
|
|
num_outputs: 2,
|
|
},
|
|
},
|
|
}),
|
|
})
|
|
.use(DropTarget, {
|
|
target: document.body,
|
|
})
|
|
|
|
if (COMPRESS) {
|
|
uppyDashboard.use(Compressor)
|
|
}
|
|
|
|
switch (UPLOADER) {
|
|
case 'tus':
|
|
uppyDashboard.use(Tus, { endpoint: TUS_ENDPOINT, limit: 6 })
|
|
break
|
|
case 's3':
|
|
uppyDashboard.use(AwsS3, {
|
|
endpoint: COMPANION_URL,
|
|
shouldUseMultipart: false,
|
|
})
|
|
break
|
|
case 's3-multipart':
|
|
uppyDashboard.use(AwsS3, {
|
|
endpoint: COMPANION_URL,
|
|
shouldUseMultipart: true,
|
|
})
|
|
break
|
|
case 'xhr':
|
|
uppyDashboard.use(XHRUpload, {
|
|
endpoint: XHR_ENDPOINT,
|
|
limit: 6,
|
|
bundle: false,
|
|
})
|
|
break
|
|
case 'transloadit':
|
|
uppyDashboard.use(Transloadit, {
|
|
service: TRANSLOADIT_SERVICE_URL,
|
|
waitForEncoding: true,
|
|
assemblyOptions,
|
|
})
|
|
break
|
|
case 'transloadit-s3':
|
|
uppyDashboard.use(AwsS3, { companionUrl: COMPANION_URL })
|
|
uppyDashboard.use(Transloadit, {
|
|
waitForEncoding: true,
|
|
importFromUploadURLs: true,
|
|
assemblyOptions,
|
|
})
|
|
break
|
|
case 'transloadit-xhr':
|
|
uppyDashboard.setMeta({
|
|
params: JSON.stringify({
|
|
auth: { key: TRANSLOADIT_KEY },
|
|
template_id: TRANSLOADIT_TEMPLATE,
|
|
}),
|
|
})
|
|
uppyDashboard.use(XHRUpload, {
|
|
method: 'POST',
|
|
endpoint: `${TRANSLOADIT_SERVICE_URL}/assemblies`,
|
|
allowedMetaFields: ['params'],
|
|
bundle: true,
|
|
})
|
|
break
|
|
default:
|
|
}
|
|
|
|
if (RESTORE) {
|
|
uppyDashboard.use(GoldenRetriever, { serviceWorker: true })
|
|
}
|
|
|
|
window.uppy = uppyDashboard
|
|
|
|
uppyDashboard.on('complete', (result) => {
|
|
if (result.failed.length === 0) {
|
|
console.log('Upload successful 😀')
|
|
} else {
|
|
console.warn('Upload failed 😞')
|
|
}
|
|
console.log('successful files:', result.successful)
|
|
console.log('failed files:', result.failed)
|
|
if (UPLOADER === 'transloadit') {
|
|
console.log('Transloadit result:', result.transloadit)
|
|
}
|
|
})
|
|
|
|
const modalTrigger = document.querySelector('#pick-files')
|
|
if (modalTrigger) modalTrigger.click()
|
|
}
|