feat: 小程序剧本、个人信息页、AI 运行时及 TTS 服务优化
This commit is contained in:
@@ -44,6 +44,33 @@ const parseSseFrame = (frame) => {
|
||||
}
|
||||
}
|
||||
|
||||
const findOverlapLength = (current, next) => {
|
||||
const max = Math.min(current.length, next.length)
|
||||
for (let size = max; size > 0; size--) {
|
||||
if (current.slice(-size) === next.slice(0, size)) return size
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const mergeStreamOutput = (current, chunk) => {
|
||||
const next = String(chunk || '')
|
||||
if (!next) return { output: current, delta: '' }
|
||||
if (!current) return { output: next, delta: next }
|
||||
if (next === current) return { output: current, delta: '' }
|
||||
if (next.length >= 16 && current.endsWith(next)) return { output: current, delta: '' }
|
||||
if (next.startsWith(current)) {
|
||||
return { output: next, delta: next.slice(current.length) }
|
||||
}
|
||||
const currentIndex = next.length > current.length ? next.indexOf(current) : -1
|
||||
if (currentIndex >= 0) {
|
||||
return { output: next, delta: next.slice(currentIndex + current.length) }
|
||||
}
|
||||
const overlap = findOverlapLength(current, next)
|
||||
if (overlap < 8) return { output: current + next, delta: next }
|
||||
const delta = next.slice(overlap)
|
||||
return { output: current + delta, delta }
|
||||
}
|
||||
|
||||
const queryRuntimeResult = (requestId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
@@ -113,6 +140,7 @@ export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone
|
||||
let requestTask
|
||||
let recoveryTimer
|
||||
let recoveryPromise
|
||||
let streamStarted = false
|
||||
|
||||
const clearRecoveryTimer = () => {
|
||||
if (recoveryTimer) {
|
||||
@@ -130,6 +158,7 @@ export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone
|
||||
|
||||
const completeFromRecoveredOutput = async () => {
|
||||
if (closed) return
|
||||
if (streamStarted || output.trim()) return
|
||||
try {
|
||||
const recoveredOutput = await recoverOnce()
|
||||
if (closed) return
|
||||
@@ -149,7 +178,7 @@ export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone
|
||||
clearRecoveryTimer()
|
||||
recoveryTimer = setTimeout(() => {
|
||||
completeFromRecoveredOutput()
|
||||
}, 8000)
|
||||
}, 25000)
|
||||
}
|
||||
|
||||
const finishRecovered = (message, event) => {
|
||||
@@ -172,7 +201,6 @@ export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone
|
||||
|
||||
const recoverOrFail = async (message, event) => {
|
||||
if (closed) return
|
||||
if (finishRecovered(message, event)) return
|
||||
try {
|
||||
const recoveredOutput = await recoverOnce()
|
||||
if (closed) return
|
||||
@@ -182,6 +210,7 @@ export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone
|
||||
resolve({ output })
|
||||
} catch (recoverError) {
|
||||
if (closed) return
|
||||
if (finishRecovered(message || recoverError.message, event)) return
|
||||
const finalMessage = message || recoverError.message || 'AI 生成结果暂时没有返回'
|
||||
closed = true
|
||||
clearRecoveryTimer()
|
||||
@@ -196,10 +225,7 @@ export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone
|
||||
|
||||
const failWithoutRecovery = (message, event) => {
|
||||
if (closed) return
|
||||
closed = true
|
||||
clearRecoveryTimer()
|
||||
onError?.(message, event)
|
||||
reject(new Error(message))
|
||||
recoverOrFail(message, event)
|
||||
}
|
||||
|
||||
const finishWithOutputOrRecover = async () => {
|
||||
@@ -247,6 +273,8 @@ export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone
|
||||
|
||||
requestTask?.onChunkReceived?.((res) => {
|
||||
try {
|
||||
streamStarted = true
|
||||
clearRecoveryTimer()
|
||||
consumeText(decodeChunk(res.data), failStream)
|
||||
} catch (error) {
|
||||
failStream(error.message || 'AI流式请求失败')
|
||||
@@ -262,11 +290,20 @@ export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone
|
||||
const event = parseSseFrame(frame)
|
||||
if (!event) return
|
||||
if (event.type === 'start') {
|
||||
streamStarted = true
|
||||
clearRecoveryTimer()
|
||||
onStart?.(event)
|
||||
} else if (event.type === 'delta') {
|
||||
output += event.content || ''
|
||||
onDelta?.(event.content || '', output, event)
|
||||
streamStarted = true
|
||||
clearRecoveryTimer()
|
||||
const merged = mergeStreamOutput(output, event.content)
|
||||
output = merged.output
|
||||
if (merged.delta) {
|
||||
onDelta?.(merged.delta, output, event)
|
||||
}
|
||||
} else if (event.type === 'done') {
|
||||
streamStarted = true
|
||||
clearRecoveryTimer()
|
||||
onDone?.(event, output)
|
||||
} else if (event.type === 'error') {
|
||||
const message = event.message || event.code || 'AI流式请求失败'
|
||||
|
||||
@@ -3,6 +3,8 @@ import { getEnvValue } from '../config/env.js'
|
||||
|
||||
const DEFAULT_SOURCE_TYPE = 'epic_script'
|
||||
const DEFAULT_VOICE = 'default_zh_female'
|
||||
const DEFAULT_SPEECH_RATE = 0.92
|
||||
const DEFAULT_EMOTION = 'story'
|
||||
|
||||
const normalizeAudioUrl = (task) => {
|
||||
if (!task?.audioUrl || /^https?:\/\//.test(task.audioUrl)) {
|
||||
@@ -25,9 +27,12 @@ const normalizeResponse = (response) => {
|
||||
export const createTtsTask = ({
|
||||
sourceType = DEFAULT_SOURCE_TYPE,
|
||||
sourceId,
|
||||
voice = DEFAULT_VOICE
|
||||
voice = DEFAULT_VOICE,
|
||||
speechRate = DEFAULT_SPEECH_RATE,
|
||||
pitch = 0,
|
||||
emotion = DEFAULT_EMOTION
|
||||
}) => {
|
||||
return post('/tts/tasks', { sourceType, sourceId, voice }).then(normalizeResponse)
|
||||
return post('/tts/tasks', { sourceType, sourceId, voice, speechRate, pitch, emotion }).then(normalizeResponse)
|
||||
}
|
||||
|
||||
export const getTtsTask = (id) => {
|
||||
@@ -37,9 +42,12 @@ export const getTtsTask = (id) => {
|
||||
export const getTtsTaskBySource = ({
|
||||
sourceType = DEFAULT_SOURCE_TYPE,
|
||||
sourceId,
|
||||
voice = DEFAULT_VOICE
|
||||
voice = DEFAULT_VOICE,
|
||||
speechRate = DEFAULT_SPEECH_RATE,
|
||||
pitch = 0,
|
||||
emotion = DEFAULT_EMOTION
|
||||
}) => {
|
||||
return get('/tts/tasks/by-source', { sourceType, sourceId, voice }).then(normalizeResponse)
|
||||
return get('/tts/tasks/by-source', { sourceType, sourceId, voice, speechRate, pitch, emotion }).then(normalizeResponse)
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
Reference in New Issue
Block a user