mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 23:25:18 +00:00
158 lines
5.7 KiB
JavaScript
158 lines
5.7 KiB
JavaScript
/**
|
|
* Pure derivation for `scripts/fill-image.mjs` — argument parsing, canvas
|
|
* extension math and prompt defaults, unit-tested like the rest of
|
|
* `scripts/lib/`. The CLI itself only threads sharp + the DeepInfra call
|
|
* through these decisions.
|
|
*/
|
|
|
|
/** DeepInfra model that fills masked regions with text guidance. */
|
|
export const FILL_MODEL = 'black-forest-labs/FLUX-2-dev'
|
|
|
|
/** Native DeepInfra inference endpoint for `FILL_MODEL` (accepts arbitrary multiples of 64). */
|
|
export const FILL_INFERENCE_URL = `https://api.deepinfra.com/v1/inference/${FILL_MODEL}`
|
|
|
|
/** When the photo is extended, the white strip below is the void to fill — this is the prompt. */
|
|
export const EXTEND_DEFAULT_PROMPT =
|
|
'Continue a imagem apenas na área branca abaixo: complete o corpo da pessoa de forma realista e coerente com a cena, com as mesmas roupas, iluminação, cores e estilo da foto, sem elementos novos.'
|
|
|
|
/** When an explicit mask is given, only the marked (whitened) region is regenerated. */
|
|
export const MASK_DEFAULT_PROMPT =
|
|
'Regenere apenas a área branca marcada, de forma natural e coerente com a cena, preservando roupas, iluminação, cores e estilo da foto original.'
|
|
|
|
/**
|
|
* Parses `--extend` values: a plain integer is pixels, `30%` is a share of
|
|
* the photo height. Returns null when the value is not one of those shapes.
|
|
*
|
|
* @param {string} value
|
|
* @returns {number | null}
|
|
*/
|
|
export const parseExtendPx = (value) => {
|
|
const percent = /^(\d{1,3})%$/.exec(value.trim())
|
|
if (percent) return Number(percent[1])
|
|
const px = Number(value.trim())
|
|
if (Number.isInteger(px) && px > 0) return px
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Bottom-strip extension for a canvas. `extendPx` may be pixels or a
|
|
* percentage of the photo height (already resolved by the caller via
|
|
* `parseExtendPx` + height).
|
|
*
|
|
* @param {number} width
|
|
* @param {number} height
|
|
* @param {number} extendPx
|
|
* @returns {{ width: number, height: number, offsetY: number }}
|
|
*/
|
|
export const extendedCanvas = (width, height, extendPx) => ({
|
|
width,
|
|
height: height + extendPx,
|
|
offsetY: height,
|
|
})
|
|
|
|
/**
|
|
* The FLUX VAE needs dimensions that are multiples of 64. Pads a canvas up
|
|
* to the next multiple on each axis; the padding is masked OPAQUE
|
|
* (preserved) and cropped away after the fill, so the final image is
|
|
* exactly the canvas the caller built.
|
|
*
|
|
* @param {number} width
|
|
* @param {number} height
|
|
* @returns {{ width: number, height: number, padRight: number, padBottom: number }}
|
|
*/
|
|
export const padToMultipleOf64 = (width, height) => ({
|
|
width: Math.ceil(width / 64) * 64,
|
|
height: Math.ceil(height / 64) * 64,
|
|
padRight: Math.ceil(width / 64) * 64 - width,
|
|
padBottom: Math.ceil(height / 64) * 64 - height,
|
|
})
|
|
|
|
const VALUE_FLAGS = new Set(['--photo', '--mask', '--extend', '--prompt', '--out', '--steps'])
|
|
|
|
const BOOLEAN_FLAGS = new Set(['--no-resize'])
|
|
|
|
const HELP_FLAGS = new Set(['--help', '-h'])
|
|
|
|
/**
|
|
* Parses CLI argv into options. Throws with a human-readable message on any
|
|
* unknown flag or invalid value (fail closed — a typo must not silently
|
|
* fall back to a default).
|
|
*
|
|
* @param {string[]} argv - `process.argv.slice(2)`
|
|
* @returns {{ photo: string, mask: string | null, extend: string | null, prompt: string, steps: number, outDir: string, resize: boolean, help: boolean }}
|
|
*/
|
|
export const parseFillArgs = (argv) => {
|
|
const options = {
|
|
help: false,
|
|
photo: undefined,
|
|
mask: null,
|
|
extend: null,
|
|
prompt: undefined,
|
|
steps: 28,
|
|
outDir: 'public',
|
|
resize: true,
|
|
}
|
|
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|
const arg = argv[i]
|
|
if (HELP_FLAGS.has(arg)) {
|
|
options.help = true
|
|
continue
|
|
}
|
|
if (BOOLEAN_FLAGS.has(arg)) {
|
|
options.resize = false
|
|
continue
|
|
}
|
|
const [flag, inlineValue] = arg.startsWith('--') ? arg.split('=') : [null, null]
|
|
if (flag !== null && VALUE_FLAGS.has(flag)) {
|
|
const value = inlineValue ?? argv[i + 1]
|
|
if (inlineValue === undefined) i += 1
|
|
if (value === undefined || value === '' || value.startsWith('--')) {
|
|
throw new Error(`flag ${flag} espera um valor (ex.: ${flag}=valor ou ${flag} valor)`)
|
|
}
|
|
if (flag === '--photo') {
|
|
options.photo = value
|
|
} else if (flag === '--mask') {
|
|
options.mask = value
|
|
} else if (flag === '--extend') {
|
|
if (parseExtendPx(value) === null) {
|
|
throw new Error(
|
|
`--extend inválido: ${JSON.stringify(value)} (esperado pixels, ex.: 400, ou percentual, ex.: 30%)`,
|
|
)
|
|
}
|
|
options.extend = value
|
|
} else if (flag === '--prompt') {
|
|
options.prompt = value
|
|
} else if (flag === '--steps') {
|
|
const steps = Number(value)
|
|
if (!Number.isInteger(steps) || steps < 1 || steps > 50) {
|
|
throw new Error(`--steps inválido: ${JSON.stringify(value)} (esperado 1..50)`)
|
|
}
|
|
options.steps = steps
|
|
} else {
|
|
options.outDir = value
|
|
}
|
|
continue
|
|
}
|
|
if (flag !== null && !HELP_FLAGS.has(arg)) {
|
|
throw new Error(`flag desconhecida: ${JSON.stringify(arg)} (veja --help)`)
|
|
}
|
|
throw new Error(`argumento posicional não esperado: ${JSON.stringify(arg)} (veja --help)`)
|
|
}
|
|
|
|
if (!options.help) {
|
|
if (options.photo === undefined) {
|
|
throw new Error('--photo é obrigatório (veja --help)')
|
|
}
|
|
if (options.mask === null && options.extend === null) {
|
|
throw new Error('informe --mask (região pintada de branco) ou --extend (aumentar embaixo)')
|
|
}
|
|
if (options.mask !== null && options.extend !== null) {
|
|
throw new Error('--mask e --extend são mutuamente exclusivos — escolha um')
|
|
}
|
|
options.prompt =
|
|
options.prompt ?? (options.extend !== null ? EXTEND_DEFAULT_PROMPT : MASK_DEFAULT_PROMPT)
|
|
}
|
|
|
|
return options
|
|
}
|