180 lines
4.9 KiB
JavaScript
180 lines
4.9 KiB
JavaScript
/**
|
||
* 镭射卡五图批量生成(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,
|
||
}
|
||
}
|