topfans/frontend/composables/useLaserBatchGenerate.js
2026-06-04 01:42:29 +08:00

180 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 镭射卡五图批量生成Phase 1纯客户端 Canvas无后端接口
*/
import { ref } from 'vue'
import {
CASTLOVE_FORM_KEY,
consumeGenerationFlowPayload,
persistLaserPreviewImages,
} from '@/utils/castloveGenerationFlow.js'
import { generateLaserVariantBatch } from '@/utils/laser-card/laserBatchExport.js'
import { segmentPortrait } from '@/composables/useLaserSegment.js'
import { useLaserDifyGenerate } from '@/composables/useLaserDifyGenerate.js'
const DEFAULT_CANVAS_ID = 'laserBatchCanvas'
const DEFAULT_MIN_DURATION_MS = 1600
/**
* @param {Object} [options]
* @param {string} [options.canvasId]
* @param {number} [options.minDurationMs]
* @param {number} [options.flowStartedAt] 页面 onMounted 时的时间戳,用于最短展示时长
*/
export function useLaserBatchGenerate(options = {}) {
const progress = ref(0)
const running = ref(false)
const error = ref(null)
let progressTimer = null
let flowStartedAt = options.flowStartedAt ?? Date.now()
const canvasId = options.canvasId || DEFAULT_CANVAS_ID
const minDurationMs = options.minDurationMs ?? DEFAULT_MIN_DURATION_MS
const stopProgress = () => {
if (progressTimer) {
clearInterval(progressTimer)
progressTimer = null
}
}
const simulateProgress = () => {
stopProgress()
progressTimer = setInterval(() => {
if (progress.value < 90) {
const increment = Math.random() * 5 + 2
progress.value = Math.min(90, progress.value + increment)
}
}, 500)
}
const completeProgress = () =>
new Promise((resolve) => {
stopProgress()
const finalInterval = setInterval(() => {
if (progress.value < 100) {
progress.value = Math.min(100, progress.value + 5)
} else {
clearInterval(finalInterval)
setTimeout(resolve, 500)
}
}, 50)
})
const waitMinDuration = async (minMs) => {
if (!minMs || minMs <= 0) return
const elapsed = Date.now() - flowStartedAt
if (elapsed < minMs) {
await new Promise((r) => setTimeout(r, minMs - elapsed))
}
}
const readFormData = () => {
const formStr = uni.getStorageSync(CASTLOVE_FORM_KEY)
if (!formStr) {
throw new Error('缺少表单数据')
}
return JSON.parse(formStr)
}
const resolveImagePath = (formData) => {
const imagePath = formData?.image || formData?.uploadedImage || ''
if (!imagePath) {
throw new Error('缺少上传图片')
}
return imagePath
}
/**
* 执行五图合成并写入 Storage
* @param {Object} [runOptions]
* @param {Object} [runOptions.flow] consumeGenerationFlowPayload 结果;缺省则自动 consume
* @returns {Promise<string[]>} 本地临时 JPG 路径列表
*/
const run = async (runOptions = {}) => {
if (running.value) {
throw new Error('生成进行中')
}
running.value = true
error.value = null
simulateProgress()
try {
const flow = runOptions.flow ?? consumeGenerationFlowPayload()
const minMs = flow?.minDurationMs ?? minDurationMs
const formData = readFormData()
const imagePath = resolveImagePath(formData)
// Dify 模式:服务端 AI 生成
const genMode ='dify'
if (genMode === 'dify') {
const dify = useLaserDifyGenerate()
const userPrompt = (runOptions.userPrompt || formData?.userPrompt || '').trim()
// 先做人像抠图,拿到 PNG 的 OSS 签名地址传给 compositor 叠加
let cutoutUrl = ''
try {
const cutout = await segmentPortrait(imagePath, {
imageBase64: formData.imageBase64 || formData.uploadedImageBase64 || '',
})
cutoutUrl = cutout?.cutoutUrl || ''
} catch (segErr) {
console.warn('[useLaserBatchGenerate] 抠图失败,将不叠加人像:', segErr)
}
await dify.submit(cutoutUrl, null, userPrompt)
const infos = dify.getVariantInfos()
const instanceNoVal = dify.getInstanceNo()
await waitMinDuration(minMs)
persistLaserPreviewImages(infos, instanceNoVal)
await completeProgress()
return infos
}
// Client 模式:本地 Canvas 合成
// 人像抠图 → 透明 PNG 叠在底纹之上,再合成五图
const cutout = await segmentPortrait(imagePath, {
imageBase64: formData.imageBase64 || formData.uploadedImageBase64 || '',
})
// 如果抠图成功,更新 formData 中的 cutoutImage
if (cutout?.localPath) {
formData.cutoutImage = cutout.localPath
}
const paths = await generateLaserVariantBatch(cutout.localPath, canvasId)
const infos = paths.map((url) => ({ url, oss_key: '' }))
await waitMinDuration(minMs)
persistLaserPreviewImages(infos)
await completeProgress()
return infos
} catch (e) {
error.value = e
stopProgress()
throw e
} finally {
running.value = false
}
}
const resetProgress = () => {
stopProgress()
progress.value = 0
}
const setFlowStartedAt = (ts) => {
flowStartedAt = ts
}
return {
progress,
running,
error,
simulateProgress,
completeProgress,
stopProgress,
resetProgress,
setFlowStartedAt,
run,
}
}