Files
upstream-mirror a774ac673a
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

236 lines
6.2 KiB
TypeScript

import { resolveImage } from './types'
import type {
GenerateParams,
GenerateResult,
ImageProvider,
UpscaleParams,
UpscaleResult,
} from './types'
/** Maps ControlNet mode IDs to Flux ControlNet model identifiers. */
const CONTROLNET_MODELS: Record<string, string> = {
canny: 'black-forest-labs/flux-canny-dev',
depth: 'black-forest-labs/flux-depth-dev',
// No Flux-native pose/segmentation models — fall back to canny
pose: 'black-forest-labs/flux-canny-dev',
segmentation: 'black-forest-labs/flux-depth-dev',
}
export const replicate: ImageProvider = {
name: 'replicate',
async generate(params: GenerateParams, env: Env): Promise<GenerateResult> {
const apiToken = env.REPLICATE_API_TOKEN
if (!apiToken) {
throw new Error('REPLICATE_API_TOKEN is not configured')
}
// Use a ControlNet model when ControlNet params are present
if (params.controlNetMode && params.referenceImageUrl) {
return generateWithControlNet(params, apiToken, env)
}
// Google models (Nano Banana, Imagen)
if (
params.modelId === 'nano-banana-pro' ||
params.modelId === 'nano-banana' ||
params.modelId === 'imagen-4-fast'
) {
return generateWithGoogle(params, apiToken)
}
return generateWithFlux(params, apiToken)
},
async upscale(params: UpscaleParams, env: Env): Promise<UpscaleResult> {
const apiToken = env.REPLICATE_API_TOKEN
if (!apiToken) {
throw new Error('REPLICATE_API_TOKEN is not configured')
}
const response = await fetch(
'https://api.replicate.com/v1/models/nightmareai/real-esrgan/predictions',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiToken}`,
Prefer: 'wait',
},
body: JSON.stringify({
input: {
image: params.imageUrl,
scale: params.scale,
},
}),
}
)
if (!response.ok) {
const text = await response.text()
throw new Error(`Replicate upscale error ${response.status}: ${text}`)
}
const data = (await response.json()) as { output: string }
return { imageUrl: data.output }
},
}
async function generateWithFlux(params: GenerateParams, apiToken: string): Promise<GenerateResult> {
const replicateModel =
params.modelId === 'flux-schnell'
? 'black-forest-labs/flux-schnell'
: params.modelId === 'flux-pro'
? 'black-forest-labs/flux-1.1-pro'
: 'black-forest-labs/flux-dev'
const response = await fetch(
`https://api.replicate.com/v1/models/${replicateModel}/predictions`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiToken}`,
Prefer: 'wait',
},
body: JSON.stringify({
input: {
prompt: params.prompt,
num_inference_steps: params.steps ?? 20,
guidance: params.cfgScale ?? 7,
seed: params.seed ?? null,
aspect_ratio: '1:1',
// Flux Pro uses safety_tolerance (1=strictest), others use disable_safety_checker
...(params.modelId === 'flux-pro'
? { safety_tolerance: 1 }
: { disable_safety_checker: false }),
...(params.referenceImageUrl ? { image: params.referenceImageUrl } : {}),
},
}),
}
)
if (!response.ok) {
const text = await response.text()
throw new Error(`Replicate error ${response.status}: ${text}`)
}
const data = (await response.json()) as {
output: string | string[]
seed?: number
}
const imageUrl = Array.isArray(data.output) ? data.output[0] : data.output
return {
imageUrl,
seed: data.seed ?? params.seed ?? 0,
}
}
/**
* Use a Google model on Replicate (Nano Banana, Imagen).
*/
async function generateWithGoogle(
params: GenerateParams,
apiToken: string
): Promise<GenerateResult> {
const modelMap: Record<string, string> = {
'nano-banana-pro': 'google/nano-banana-pro',
'nano-banana': 'google/nano-banana',
'imagen-4-fast': 'google/imagen-4-fast',
}
const replicateModel = modelMap[params.modelId] ?? 'google/nano-banana-pro'
const response = await fetch(
`https://api.replicate.com/v1/models/${replicateModel}/predictions`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiToken}`,
Prefer: 'wait',
},
body: JSON.stringify({
input: {
prompt: params.prompt,
aspect_ratio: '1:1',
...(params.referenceImageUrl ? { image_input: [params.referenceImageUrl] } : {}),
},
}),
}
)
if (!response.ok) {
const text = await response.text()
throw new Error(`Replicate Google model error ${response.status}: ${text}`)
}
const data = (await response.json()) as {
output: string | string[]
seed?: number
}
const imageUrl = Array.isArray(data.output) ? data.output[0] : data.output
return {
imageUrl,
seed: data.seed ?? params.seed ?? 0,
}
}
/**
* Use a Flux ControlNet model on Replicate.
* The reference image is passed as the control image, and the mode
* selects the appropriate variant (canny or depth).
*/
async function generateWithControlNet(
params: GenerateParams,
apiToken: string,
env: Env
): Promise<GenerateResult> {
const model = CONTROLNET_MODELS[params.controlNetMode!] ?? CONTROLNET_MODELS.canny
// Replicate accepts https URLs and data URIs directly.
// Only resolve R2 paths that Replicate can't access.
let imageInput = params.referenceImageUrl!
if (imageInput.startsWith('/api/images/')) {
const { dataUrl } = await resolveImage(imageInput, env)
imageInput = dataUrl
}
const response = await fetch(`https://api.replicate.com/v1/models/${model}/predictions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiToken}`,
Prefer: 'wait',
},
body: JSON.stringify({
input: {
control_image: imageInput,
prompt: params.prompt,
num_inference_steps: params.steps ?? 28,
guidance: params.cfgScale ?? 30,
...(params.seed != null ? { seed: params.seed } : {}),
output_format: 'png',
disable_safety_checker: false,
},
}),
})
if (!response.ok) {
const text = await response.text()
throw new Error(`Replicate ControlNet error ${response.status}: ${text}`)
}
const data = (await response.json()) as {
output: string | string[]
seed?: number
}
const imageUrl = Array.isArray(data.output) ? data.output[0] : data.output
return {
imageUrl,
seed: data.seed ?? params.seed ?? 0,
}
}