├── .gitignore ├── .yarnrc.yml ├── README.md ├── app.config.ts ├── app.vue ├── components ├── PredictionPoller.vue └── ui │ ├── Canvas.vue │ ├── LeftPanel.vue │ ├── TopPanel.vue │ └── form │ ├── Create.vue │ ├── Finetune.vue │ └── VersionPicker.vue ├── nuxt.config.ts ├── package.json ├── pages └── index.vue ├── prettier.config.cjs ├── public ├── cover.jpg ├── favicon.ico └── replicate-logo.svg ├── server ├── api │ ├── model.post.js │ ├── prediction.get.js │ ├── prediction.post.js │ ├── search.get.js │ └── training.post.js └── tsconfig.json ├── stores ├── prediction.js └── version.js ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | .yarn 21 | 22 | # Local env files 23 | .env 24 | .env.* 25 | !.env.example 26 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReFlux 2 | 3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install the dependencies: 8 | 9 | ```bash 10 | # npm 11 | npm install 12 | 13 | # pnpm 14 | pnpm install 15 | 16 | # yarn 17 | yarn install 18 | 19 | # bun 20 | bun install 21 | ``` 22 | 23 | ## Development Server 24 | 25 | Start the development server on `http://localhost:3000`: 26 | 27 | ```bash 28 | # npm 29 | npm run dev 30 | 31 | # pnpm 32 | pnpm run dev 33 | 34 | # yarn 35 | yarn dev 36 | 37 | # bun 38 | bun run dev 39 | ``` 40 | 41 | ## Production 42 | 43 | Build the application for production: 44 | 45 | ```bash 46 | # npm 47 | npm run build 48 | 49 | # pnpm 50 | pnpm run build 51 | 52 | # yarn 53 | yarn build 54 | 55 | # bun 56 | bun run build 57 | ``` 58 | 59 | Locally preview production build: 60 | 61 | ```bash 62 | # npm 63 | npm run preview 64 | 65 | # pnpm 66 | pnpm run preview 67 | 68 | # yarn 69 | yarn preview 70 | 71 | # bun 72 | bun run preview 73 | ``` 74 | 75 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 76 | -------------------------------------------------------------------------------- /app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | ui: { 3 | primary: 'yellow', 4 | gray: 'stone' 5 | } 6 | }) 7 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /components/PredictionPoller.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47 | -------------------------------------------------------------------------------- /components/ui/Canvas.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 1160 | 1161 | 1162 | -------------------------------------------------------------------------------- /components/ui/LeftPanel.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /components/ui/TopPanel.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 122 | 123 | 140 | -------------------------------------------------------------------------------- /components/ui/form/Create.vue: -------------------------------------------------------------------------------- 1 | 202 | 203 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /components/ui/form/Finetune.vue: -------------------------------------------------------------------------------- 1 | 111 | 112 | 275 | 276 | 281 | -------------------------------------------------------------------------------- /components/ui/form/VersionPicker.vue: -------------------------------------------------------------------------------- 1 | 90 | 91 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | compatibilityDate: '2024-04-03', 3 | runtimeConfig: { 4 | public: {} 5 | }, 6 | devtools: { enabled: false }, 7 | ssr: false, 8 | nitro: { 9 | preset: 'vercel' 10 | }, 11 | sourcemap: { 12 | server: false, 13 | client: false 14 | }, 15 | app: { 16 | head: { 17 | htmlAttrs: { 18 | class: 'h-full' 19 | }, 20 | title: 'ReFlux', 21 | link: [ 22 | { rel: 'canonical', href: 'https://reflux.replicate.dev/' }, 23 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } 24 | ], 25 | meta: [ 26 | { hid: 'charset', charset: 'utf-8' }, 27 | { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' }, 28 | { 29 | hid: 'viewport', 30 | name: 'viewport', 31 | content: 32 | 'width=device-width,height=device-height,initial-scale=1.0,user-scalable=0,minimum-scale=1.0,maximum-scale=1.0,viewport-fit=cover' 33 | }, 34 | { 35 | hid: 'format-detection', 36 | name: 'format-detection', 37 | content: 'telephone=no' 38 | }, 39 | { 40 | hid: 'description', 41 | name: 'description', 42 | content: 'Replicate Flux.1 and fine-tune editor.' 43 | }, 44 | { 45 | hid: 'og:type', 46 | name: 'og:type', 47 | property: 'og:type', 48 | content: 'website' 49 | }, 50 | { 51 | hid: 'og:url', 52 | name: 'og:url', 53 | property: 'og:url', 54 | content: 'https://reflux.replicate.dev' 55 | }, 56 | { 57 | hid: 'og:site_name', 58 | name: 'og:site_name', 59 | property: 'og:site_name', 60 | content: 'reflux.replicate.dev' 61 | }, 62 | { 63 | hid: 'og:title', 64 | name: 'og:title', 65 | property: 'og:title', 66 | content: 'ReFlux' 67 | }, 68 | { 69 | hid: 'og:description', 70 | name: 'og:description', 71 | property: 'og:description', 72 | content: 'Replicate Flux.1 and fine-tune editor.' 73 | }, 74 | { 75 | hid: 'og:image', 76 | name: 'og:image', 77 | property: 'og:image', 78 | content: 'https://reflux.replicate.dev/cover.jpg' 79 | }, 80 | { 81 | hid: 'msapplication-TileColor', 82 | name: 'msapplication-TileColor', 83 | content: '#ffffff' 84 | }, 85 | { hid: 'theme-color', name: 'theme-color', content: '#ffffff' }, 86 | { 87 | hid: 'mobile-web-app-capable', 88 | name: 'mobile-web-app-capable', 89 | content: 'yes' 90 | }, 91 | { 92 | hid: 'apple-mobile-web-app-title', 93 | name: 'apple-mobile-web-app-title', 94 | content: 'reflux.replicate.dev' 95 | } 96 | ], 97 | script: [ 98 | /* 99 | { 100 | async: true, 101 | src: `https://www.googletagmanager.com/gtag/js?id=${process.env.GTAG_ID}` 102 | }, 103 | { 104 | children: ` 105 | window.dataLayer = window.dataLayer || []; 106 | function gtag(){dataLayer.push(arguments);} 107 | gtag('js', new Date()); 108 | gtag('config', '${process.env.GTAG_ID}');` 109 | } 110 | */ 111 | ], 112 | bodyAttrs: { 113 | class: 'antialiased overscroll-x-none' 114 | } 115 | } 116 | }, 117 | modules: ['@nuxt/ui', '@pinia/nuxt', '@vueuse/nuxt'], 118 | colorMode: { 119 | preference: 'light' 120 | }, 121 | ui: { 122 | global: true 123 | } 124 | }) 125 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reflux", 3 | "author": "Pontus Aurdal", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "nuxt build", 8 | "dev": "nuxt dev", 9 | "generate": "nuxt generate", 10 | "preview": "nuxt preview", 11 | "postinstall": "nuxt prepare" 12 | }, 13 | "dependencies": { 14 | "@nuxt/ui": "^2.18.3", 15 | "@pinia/nuxt": "^0.5.2", 16 | "@vueuse/core": "^10.11.0", 17 | "@vueuse/nuxt": "^10.11.0", 18 | "all-the-public-replicate-models": "^1.310.0", 19 | "jszip": "^3.10.1", 20 | "konva": "^9.3.14", 21 | "lodash-es": "^4.17.21", 22 | "nuxt": "^3.12.4", 23 | "uuid": "^10.0.0", 24 | "vue": "latest" 25 | }, 26 | "devDependencies": { 27 | "pug": "^3.0.3", 28 | "pug-plain-loader": "^1.1.0", 29 | "stylus": "^0.63.0", 30 | "stylus-loader": "^8.1.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'none', 3 | tabWidth: 2, 4 | semi: false, 5 | singleQuote: true, 6 | bracketSpacing: true 7 | } 8 | -------------------------------------------------------------------------------- /public/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replicate/reflux/01b0ea9811abfc38677d1d590f78f0a227be3210/public/cover.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replicate/reflux/01b0ea9811abfc38677d1d590f78f0a227be3210/public/favicon.ico -------------------------------------------------------------------------------- /public/replicate-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/api/model.post.js: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(async (event) => { 2 | try { 3 | const { 4 | replicate_api_token, 5 | name, 6 | trigger_word, 7 | visibility = 'public' 8 | } = await readBody(event) 9 | 10 | // Get username 11 | let result = await fetch('https://api.replicate.com/v1/account', { 12 | method: 'GET', 13 | headers: { 14 | Authorization: `Bearer ${replicate_api_token}`, 15 | 'User-Agent': 'ReFlux/1.0' 16 | } 17 | }) 18 | const { username } = await result.json() 19 | 20 | // Check if model already exists 21 | result = await fetch( 22 | `https://api.replicate.com/v1/models/${username}/${name}`, 23 | { 24 | method: 'GET', 25 | headers: { 26 | Authorization: `Bearer ${replicate_api_token}`, 27 | 'Content-Type': 'application/json', 28 | 'User-Agent': 'ReFlux/1.0' 29 | } 30 | } 31 | ) 32 | let model = await result.json() 33 | 34 | // Create model 35 | if (!model?.name) { 36 | result = await fetch('https://api.replicate.com/v1/models', { 37 | method: 'POST', 38 | headers: { 39 | Authorization: `Bearer ${replicate_api_token}`, 40 | 'Content-Type': 'application/json', 41 | 'User-Agent': 'ReFlux/1.0' 42 | }, 43 | body: JSON.stringify({ 44 | owner: username, 45 | name, 46 | description: `A fine-tuned FLUX.1 model. Use trigger word "${ 47 | trigger_word || 'TOK' 48 | }". Created with ReFlux (https://reflux.replicate.dev).`, 49 | hardware: 'gpu-t4', 50 | visibility 51 | }) 52 | }) 53 | 54 | model = await result.json() 55 | } 56 | 57 | return model 58 | } catch (e) { 59 | console.log('--- error (api/model): ', e) 60 | 61 | return { 62 | error: e.message 63 | } 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /server/api/prediction.get.js: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(async (event) => { 2 | try { 3 | const { token, ids } = getQuery(event) 4 | const id_array = ids.split(',') 5 | 6 | const results = await Promise.all( 7 | id_array.map((id) => 8 | fetch(`https://api.replicate.com/v1/predictions/${id}`, { 9 | method: 'GET', 10 | headers: { 11 | Authorization: `Bearer ${token}`, 12 | 'User-Agent': 'ReFlux/1.0' 13 | } 14 | }) 15 | ) 16 | ) 17 | 18 | const predictions = await Promise.all( 19 | results.map((result) => result.json()) 20 | ) 21 | 22 | // Remove potentially long data 23 | // delete json.logs 24 | 25 | return predictions 26 | } catch (e) { 27 | console.log('--- error (api/prediction): ', e) 28 | 29 | return { 30 | error: e.message 31 | } 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /server/api/prediction.post.js: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(async (event) => { 2 | try { 3 | const { replicate_api_token, version, input } = await readBody(event) 4 | 5 | const result = await fetch('https://api.replicate.com/v1/predictions', { 6 | method: 'POST', 7 | headers: { 8 | Authorization: `Bearer ${replicate_api_token}`, 9 | 'User-Agent': 'ReFlux/1.0' 10 | }, 11 | body: JSON.stringify({ 12 | version, 13 | input 14 | }) 15 | }) 16 | 17 | const prediction = await result.json() 18 | 19 | return prediction 20 | } catch (e) { 21 | console.log('--- error (api/prediction): ', e) 22 | 23 | return { 24 | error: e.message 25 | } 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /server/api/search.get.js: -------------------------------------------------------------------------------- 1 | import models from 'all-the-public-replicate-models' 2 | 3 | const getTriggerWord = (model) => { 4 | const description = model?.description || '' 5 | const default_example_prompt = model?.default_example?.input.prompt || '' 6 | const prompt_description = 7 | model?.latest_version?.openapi_schema?.components?.schemas?.Input 8 | ?.properties?.prompt?.description || '' 9 | 10 | const checkPatterns = (str) => { 11 | // Regular expressions for different trigger word patterns 12 | const quotedPattern = /"([^"]+)"|'([^']+)'/ 13 | const allCapsPattern = /\b[A-Z]{2,}\b/ 14 | const stylePattern = /(\S+(?:\s+\S+)*)\s+style/i 15 | 16 | // Check for quoted words 17 | const quotedMatch = str.match(quotedPattern) 18 | if (quotedMatch) return quotedMatch[1] || quotedMatch[2] 19 | 20 | // Check for all-caps words 21 | const allCapsMatch = str.match(allCapsPattern) 22 | if (allCapsMatch) return allCapsMatch[0] 23 | 24 | // Check for words followed by 'style' 25 | // const styleMatch = str.match(stylePattern) 26 | // if (styleMatch) return styleMatch[1] 27 | 28 | return null 29 | } 30 | 31 | let word = null 32 | word = checkPatterns(description) 33 | if (word) return word 34 | word = checkPatterns(prompt_description) 35 | if (word) return word 36 | word = checkPatterns(default_example_prompt) 37 | if (word) return word 38 | 39 | // Default return if no trigger word is found 40 | return 'TOK' 41 | } 42 | 43 | export default defineEventHandler(async (event) => { 44 | try { 45 | const { token } = getQuery(event) 46 | 47 | // Get trainings 48 | // TODO: pagination 49 | const result = await fetch(`https://api.replicate.com/v1/trainings`, { 50 | method: 'GET', 51 | headers: { 52 | Authorization: `Bearer ${token}`, 53 | 'User-Agent': 'ReFlux/1.0' 54 | } 55 | }) 56 | const { results } = await result.json() 57 | 58 | const owner = ( 59 | await Promise.all( 60 | (results || []).map(async (training) => { 61 | try { 62 | // Not a flux training 63 | if ( 64 | training?.model !== 'ostris/flux-dev-lora-trainer' || 65 | training?.status === 'failed' 66 | ) { 67 | return null 68 | } 69 | 70 | const [full_name, version] = training?.output?.version.split(':') 71 | const [username, name] = full_name.split('/') 72 | const model_result = await fetch( 73 | `https://api.replicate.com/v1/models/${username}/${name}`, 74 | { 75 | method: 'GET', 76 | headers: { 77 | Authorization: `Bearer ${token}`, 78 | 'Content-Type': 'application/json', 79 | 'User-Agent': 'ReFlux/1.0' 80 | } 81 | } 82 | ) 83 | const model = await model_result.json() 84 | 85 | return ( 86 | (model?.name || '') + 87 | ' ' + 88 | (model?.description || '') 89 | ).includes('flux') 90 | ? { 91 | is_owner: true, 92 | owner: model.owner, 93 | name: model.name, 94 | description: model?.description || '', 95 | version: model?.latest_version?.id, 96 | cover_image_url: model?.cover_image_url || null, 97 | trigger: getTriggerWord(model) 98 | } 99 | : null 100 | } catch (e) { 101 | console.log(e) 102 | return null 103 | } 104 | }) 105 | ) 106 | ).filter((v) => !!v) 107 | 108 | // Use all-the-public-replicate-models package instead 109 | const _public = models 110 | .filter( 111 | (i) => 112 | ((i?.name || '') + ' ' + (i?.description || '')).includes('flux') && 113 | i?.latest_version?.openapi_schema?.components?.schemas?.TrainingInput 114 | ) 115 | .map((i) => ({ 116 | is_owner: false, 117 | owner: i.owner, 118 | name: i.name, 119 | description: i?.description || '', 120 | version: i?.latest_version?.id, 121 | cover_image_url: i?.cover_image_url || null, 122 | trigger: getTriggerWord(i) 123 | })) 124 | 125 | return [...owner, ..._public] 126 | } catch (e) { 127 | console.log('--- error (api/search): ', e) 128 | 129 | return { 130 | error: e.message 131 | } 132 | } 133 | }) 134 | -------------------------------------------------------------------------------- /server/api/training.post.js: -------------------------------------------------------------------------------- 1 | import JSZip from 'jszip' 2 | 3 | const downloadAndZipImages = async (urls) => { 4 | const zip = new JSZip() 5 | const promises = urls.map(async (url) => { 6 | const response = await fetch(url) 7 | const arrayBuffer = await response.arrayBuffer() 8 | const fileName = url.split('/').pop() 9 | zip.file(fileName, arrayBuffer) 10 | }) 11 | await Promise.all(promises) 12 | const zipBlob = await zip.generateAsync({ type: 'blob' }) 13 | const arrayBuffer = await zipBlob.arrayBuffer() 14 | const base64data = Buffer.from(arrayBuffer).toString('base64') 15 | return `data:application/zip;base64,${base64data}` 16 | } 17 | 18 | export default defineEventHandler(async (event) => { 19 | try { 20 | const { replicate_api_token, destination, input } = await readBody(event) 21 | 22 | // Download and ZIP files 23 | const input_images_base64 = await downloadAndZipImages(input?.input_images) 24 | 25 | const result = await fetch( 26 | 'https://api.replicate.com/v1/models/ostris/flux-dev-lora-trainer/versions/7f53f82066bcdfb1c549245a624019c26ca6e3c8034235cd4826425b61e77bec/trainings', 27 | { 28 | method: 'POST', 29 | headers: { 30 | Authorization: `Bearer ${replicate_api_token}`, 31 | 'User-Agent': 'ReFlux/1.0' 32 | }, 33 | body: JSON.stringify({ 34 | destination, 35 | input: { 36 | ...input, 37 | input_images: input_images_base64 38 | } 39 | }) 40 | } 41 | ) 42 | 43 | const prediction = await result.json() 44 | 45 | return prediction 46 | } catch (e) { 47 | console.log('--- error (api/prediction): ', e) 48 | 49 | return { 50 | error: e.message 51 | } 52 | } 53 | }) 54 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /stores/prediction.js: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '@vueuse/core' 2 | 3 | import { useVersionStore } from './version' 4 | 5 | const parseAspectRatio = (aspectRatio) => { 6 | const [width, height] = aspectRatio.split(':').map(Number) 7 | return { width, height } 8 | } 9 | 10 | const urlToBase64 = async (urlOrArray) => { 11 | const convertSingle = async (url) => { 12 | if (typeof url !== 'string' || !url.startsWith('https')) { 13 | return url 14 | } 15 | 16 | try { 17 | // Try to extract file extension from URL 18 | const urlParts = url.split('/') 19 | const fileName = urlParts[urlParts.length - 1] 20 | const fileExtension = fileName.split('.').pop().toLowerCase() 21 | 22 | const response = await fetch(url) 23 | const blob = await response.blob() 24 | 25 | // Determine file type 26 | let fileType 27 | if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExtension)) { 28 | fileType = `image/${fileExtension === 'jpg' ? 'jpeg' : fileExtension}` 29 | } else { 30 | fileType = blob.type || 'application/octet-stream' 31 | } 32 | 33 | return new Promise((resolve, reject) => { 34 | const reader = new FileReader() 35 | reader.onloadend = () => { 36 | const base64data = reader.result 37 | const dataUri = `data:${fileType};base64,${base64data.split(',')[1]}` 38 | resolve(dataUri) 39 | } 40 | reader.onerror = reject 41 | reader.readAsDataURL(blob) 42 | }) 43 | } catch (error) { 44 | console.error('Error converting URL to base64:', error) 45 | return url 46 | } 47 | } 48 | 49 | if (Array.isArray(urlOrArray)) { 50 | return Promise.all(urlOrArray.map(convertSingle)) 51 | } else { 52 | return convertSingle(urlOrArray) 53 | } 54 | } 55 | 56 | export const usePredictionStore = defineStore('predictionStore', { 57 | state: () => ({ 58 | replicate_api_token: useLocalStorage('reflux-replicate-api-token', null), 59 | outputs: useLocalStorage('reflux-outputs', []), 60 | trainings: useLocalStorage('reflux-trainings', []) 61 | }), 62 | actions: { 63 | async createBatch({ versions, hf_versions, num_outputs, merge, input }) { 64 | try { 65 | const versionStore = useVersionStore() 66 | const combined_versions = [...versions, ...hf_versions] 67 | 68 | let promises = [] 69 | 70 | // Merging, use num_outputs 71 | if (combined_versions.length === 2 && merge) { 72 | // Use the Replicate model + side car 73 | if (versions.length > 0) { 74 | const extra_lora = 75 | hf_versions.length > 0 76 | ? `huggingface.co/${hf_versions[0]}` 77 | : versionStore.getOwnerNameByVersion(versions[1]) 78 | 79 | promises = Array.from(Array(num_outputs).keys()).map(() => 80 | $fetch('/api/prediction', { 81 | method: 'POST', 82 | body: { 83 | replicate_api_token: this.replicate_api_token, 84 | version: versions[0], 85 | input: { 86 | ...input, 87 | extra_lora, 88 | extra_lora_scale: input.lora_scale, // For now 89 | seed: Math.floor(Math.random() * 1000) 90 | } 91 | } 92 | }) 93 | ) 94 | // Use lucataco/flux-dev-multi-lora 95 | } else { 96 | promises = Array.from(Array(num_outputs).keys()).map(() => 97 | $fetch('/api/prediction', { 98 | method: 'POST', 99 | body: { 100 | replicate_api_token: this.replicate_api_token, 101 | // https://replicate.com/lucataco/flux-dev-multi-lora 102 | version: 103 | 'a738942df15c8c788b076ddd052256ba7923aade687b12109ccc64b2c3483aa1', 104 | input: { 105 | ...input, 106 | hf_loras: hf_versions, 107 | lora_scales: Array.from( 108 | Array(hf_versions.length).keys() 109 | ).map(() => input.lora_scale), 110 | seed: Math.floor(Math.random() * 1000) 111 | } 112 | } 113 | }) 114 | ) 115 | } 116 | // Not merging, use num_outputs 117 | } else if (combined_versions.length < 2) { 118 | if (versions.length > 0) { 119 | promises = Array.from(Array(num_outputs).keys()).map(() => 120 | $fetch('/api/prediction', { 121 | method: 'POST', 122 | body: { 123 | replicate_api_token: this.replicate_api_token, 124 | version: versions[0], 125 | input: { 126 | ...input, 127 | seed: Math.floor(Math.random() * 1000) 128 | } 129 | } 130 | }) 131 | ) 132 | } else if (hf_versions.length > 0) { 133 | promises = Array.from(Array(num_outputs).keys()).map(() => 134 | $fetch('/api/prediction', { 135 | method: 'POST', 136 | body: { 137 | replicate_api_token: this.replicate_api_token, 138 | // https://replicate.com/lucataco/flux-dev-multi-lora 139 | version: 140 | 'a738942df15c8c788b076ddd052256ba7923aade687b12109ccc64b2c3483aa1', 141 | input: { 142 | ...input, 143 | hf_loras: [hf_versions[0]], 144 | lora_scales: [input.lora_scale], 145 | seed: Math.floor(Math.random() * 1000) 146 | } 147 | } 148 | }) 149 | ) 150 | } 151 | 152 | // Not merging, create one of each 153 | } else { 154 | promises.push( 155 | ...versions.map((version) => 156 | $fetch('/api/prediction', { 157 | method: 'POST', 158 | body: { 159 | replicate_api_token: this.replicate_api_token, 160 | version, 161 | input 162 | } 163 | }) 164 | ), 165 | ...hf_versions.map((hf_version) => 166 | $fetch('/api/prediction', { 167 | method: 'POST', 168 | body: { 169 | replicate_api_token: this.replicate_api_token, 170 | // https://replicate.com/lucataco/flux-dev-multi-lora 171 | version: 172 | 'a738942df15c8c788b076ddd052256ba7923aade687b12109ccc64b2c3483aa1', 173 | input: { 174 | ...input, 175 | hf_loras: [hf_version], 176 | lora_scales: [input.lora_scale] 177 | } 178 | } 179 | }) 180 | ) 181 | ) 182 | } 183 | 184 | const predictions = await Promise.all(promises) 185 | 186 | const baseSize = 300 187 | const aspectRatio = parseAspectRatio(input.aspect_ratio) 188 | const width = baseSize 189 | const height = (aspectRatio.width / aspectRatio.height) * baseSize 190 | 191 | predictions.forEach((prediction, index) => { 192 | this.outputs.push({ 193 | id: prediction.id, 194 | status: prediction.status, 195 | input, 196 | output: null, 197 | metadata: { 198 | prediction_id: prediction.id, 199 | name: versionStore.getOwnerNameByVersion(prediction.version), 200 | x: 0, 201 | y: 0, 202 | rotation: 0, 203 | width, 204 | height 205 | } 206 | }) 207 | }) 208 | 209 | return predictions 210 | } catch (e) { 211 | console.log('--- (stores/prediction) error:', e.message) 212 | } 213 | }, 214 | async createTrainingPrestep({ name, trigger_word, visibility, input }) { 215 | try { 216 | const model = await $fetch('/api/model', { 217 | method: 'POST', 218 | body: { 219 | replicate_api_token: this.replicate_api_token, 220 | name, 221 | trigger_word, 222 | visibility 223 | } 224 | }) 225 | 226 | if (!model?.owner) { 227 | throw new Error('failed to create model') 228 | } 229 | 230 | const prediction = await $fetch('/api/prediction', { 231 | method: 'POST', 232 | body: { 233 | replicate_api_token: this.replicate_api_token, 234 | // https://replicate.com/fofr/consistent-character 235 | version: 236 | '9c77a3c2f884193fcee4d89645f02a0b9def9434f9e03cb98460456b831c8772', 237 | input: { 238 | ...input, 239 | number_of_outputs: 15, 240 | number_of_images_per_pose: 1, 241 | randomise_poses: true, 242 | output_format: 'webp', 243 | output_quality: 80 244 | } 245 | } 246 | }) 247 | 248 | this.trainings.push({ 249 | id: prediction.id, 250 | status: prediction.status, 251 | input, 252 | output: null, 253 | metadata: { 254 | prediction_id: prediction.id, 255 | pipeline_stage: 'preprocessing', 256 | trigger_word, 257 | destination: model 258 | } 259 | }) 260 | 261 | return prediction 262 | } catch (e) { 263 | console.log('--- (stores/prediction) error:', e.message) 264 | } 265 | }, 266 | async createTraining(training) { 267 | try { 268 | const { trigger_word, destination } = training?.metadata 269 | const output = training?.output || [] 270 | 271 | // Fail early 272 | if (output.length <= 0) { 273 | const index = this.trainings.findIndex((i) => i.id === training.id) 274 | if (index !== -1) { 275 | this.trainings[index].status = 'failed' 276 | } 277 | return null 278 | } 279 | 280 | const input = { 281 | input_images: output, 282 | trigger_word 283 | } 284 | 285 | const prediction = await $fetch('/api/training', { 286 | method: 'POST', 287 | body: { 288 | replicate_api_token: this.replicate_api_token, 289 | destination: `${destination?.owner}/${destination?.name}`, 290 | input 291 | } 292 | }) 293 | 294 | // Update training 295 | const index = this.trainings.findIndex((i) => i.id === training.id) 296 | if (index !== -1) { 297 | this.trainings[index].id = prediction.id 298 | this.trainings[index].status = prediction.status 299 | this.trainings[index].input = input 300 | this.trainings[index].output = null 301 | this.trainings[index].metadata.prediction_id = prediction.id 302 | this.trainings[index].metadata.pipeline_stage = 'processing' 303 | } 304 | 305 | return prediction 306 | } catch (e) { 307 | console.log('--- (stores/prediction) error:', e.message) 308 | } 309 | }, 310 | async pollIncompletePredictions() { 311 | try { 312 | const prediction_ids = [ 313 | ...new Set( 314 | this.incompletePredictions 315 | .map((output) => output?.metadata?.prediction_id || null) 316 | .filter((id) => id) 317 | ) 318 | ] 319 | const predictions = await $fetch( 320 | `/api/prediction?ids=${prediction_ids.join(',')}&token=${ 321 | this.replicate_api_token 322 | }` 323 | ) 324 | 325 | for (const prediction of predictions) { 326 | // Update outputs with the same prediction_id in the state 327 | let targets = this.outputs.filter( 328 | (i) => i?.metadata?.prediction_id === prediction.id 329 | ) 330 | 331 | for (const target of targets) { 332 | // Update the corresponding output in the state 333 | const index = this.outputs.findIndex((i) => i.id === target.id) 334 | if (index !== -1) { 335 | this.outputs[index] = { 336 | ...this.outputs[index], 337 | input: prediction.input, 338 | status: prediction.status, 339 | output: await urlToBase64(prediction.output) 340 | } 341 | } 342 | } 343 | 344 | // Update trainings with the same prediction_id in the state 345 | targets = this.trainings.filter( 346 | (i) => i?.metadata?.prediction_id === prediction.id 347 | ) 348 | 349 | for (const target of targets) { 350 | // Update the corresponding output in the state 351 | const index = this.trainings.findIndex((i) => i.id === target.id) 352 | if (index !== -1) { 353 | this.trainings[index] = { 354 | ...this.trainings[index], 355 | input: prediction.input, 356 | status: prediction.status, 357 | output: prediction.output 358 | } 359 | } 360 | } 361 | } 362 | } catch (e) { 363 | console.log( 364 | '--- (stores/prediction) error polling incomplete predictions:', 365 | e.message 366 | ) 367 | } 368 | }, 369 | updateOutputPosition({ id, x, y, rotation, width, height }) { 370 | const index = this.outputs.findIndex((output) => output.id === id) 371 | if (index !== -1) { 372 | this.outputs[index].metadata.x = x 373 | this.outputs[index].metadata.y = y 374 | this.outputs[index].metadata.rotation = rotation 375 | if (width !== undefined) this.outputs[index].metadata.width = width 376 | if (height !== undefined) this.outputs[index].metadata.height = height 377 | } 378 | }, 379 | addOutput(output) { 380 | if (Array.isArray(output)) { 381 | this.outputs.push(...output) 382 | } else { 383 | this.outputs.push(output) 384 | } 385 | }, 386 | removeOutput(id) { 387 | if (Array.isArray(id)) { 388 | this.outputs = this.outputs.filter((output) => !id.includes(output.id)) 389 | } else { 390 | this.outputs = this.outputs.filter((output) => output.id !== id) 391 | } 392 | } 393 | }, 394 | getters: { 395 | incompletePredictions: (state) => 396 | [...state.outputs, ...state.trainings].filter( 397 | (output) => output.status !== 'succeeded' && output.status !== 'failed' 398 | ) 399 | } 400 | }) 401 | -------------------------------------------------------------------------------- /stores/version.js: -------------------------------------------------------------------------------- 1 | export const useVersionStore = defineStore('versionStore', { 2 | state: () => ({ 3 | version_options: [] 4 | }), 5 | actions: { 6 | setVersionOptions(val) { 7 | this.version_options = val 8 | } 9 | }, 10 | getters: { 11 | getOwnerNameByVersion: (state) => (version) => { 12 | const item = state.version_options.find((i) => i.version === version) 13 | return item ? `${item.owner}/${item.name}` : null 14 | }, 15 | getVersionByName: (state) => (name) => { 16 | const item = state.version_options.find((i) => i.name === name) 17 | return item ? item.version : null 18 | }, 19 | getTriggerByVersion: (state) => (version) => { 20 | const item = state.version_options.find((i) => i.version === version) 21 | return item ? item.trigger : null 22 | } 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | --------------------------------------------------------------------------------