File size: 6,130 Bytes
b1df986 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | /**
* 概念分析步骤
* 分析用户输入,决定生成策略
*/
import { performance } from 'node:perf_hooks'
import { generateTwoStageAIManimCode } from '../../../../workflow/concept-design/concept-designer'
import { storeProcessingStage } from '../video-processor-utils'
import { ensureJobNotCancelled } from '../../../job/job-cancel'
import { isJobCancelled } from '../../../job/job-cancel-store'
import { cleanupNarrationWorkspace } from '../../../manim/narration-mixer'
import { createLogger } from '../../../../shared/logging/logger'
import type { NarrationAssets, NarrationConfig } from '../../../../workflow/narration/types'
import type { CustomApiConfig, OutputMode, PromptOverrides, ReferenceImage, ProblemFramingPlan } from '../../../../types'
const logger = createLogger('AnalysisStep')
/** Server-resolved narration input for one generation run. */
export interface NarrationStageRequest {
config: NarrationConfig
outputDirectory: string
}
/**
* 概念分析结果
*/
export interface AnalysisResult {
analysisType: 'latex' | 'template' | 'ai' | 'fallback'
code: string | null
needsAI: boolean
}
/**
* 代码生成结果
*/
export interface GenerationResult {
code: string
usedAI: boolean
generationType: string
sceneDesign?: string
problemPlan?: ProblemFramingPlan
referenceImages?: ReferenceImage[]
narration?: NarrationAssets
}
/**
* 分析概念
*/
export async function analyzeConcept(
jobId: string,
concept: string,
_quality: string,
outputMode: OutputMode
): Promise<AnalysisResult> {
logger.info('Analyzing concept', { jobId, concept, outputMode })
logger.info('Using AI for all concepts (template shortcuts disabled)', { jobId, outputMode })
return {
analysisType: 'ai',
code: null,
needsAI: true
}
}
/**
* 生成代码
*/
export async function generateCode(
jobId: string,
concept: string,
_quality: string,
outputMode: OutputMode,
analyzeResult: AnalysisResult,
customApiConfig?: CustomApiConfig,
promptOverrides?: PromptOverrides,
referenceImages?: ReferenceImage[],
problemPlan?: ProblemFramingPlan,
narration?: NarrationStageRequest
): Promise<GenerationResult> {
const { analysisType, code, needsAI } = analyzeResult
logger.info('Generating code', { jobId, outputMode, needsAI, analysisType, hasImages: !!referenceImages?.length })
if (needsAI) {
const narrationAbort = new AbortController()
let cancellationWatcher: NodeJS.Timeout | undefined
if (narration) {
// The LLM stages checkpoint through `ensureJobNotCancelled`; TTS is a separate child process,
// so give it a durable signal and turn the Redis marker into an abort.
cancellationWatcher = setInterval(() => {
void isJobCancelled(jobId)
.then((cancelled) => {
if (cancelled) narrationAbort.abort()
})
.catch(() => undefined)
}, 1000)
cancellationWatcher.unref?.()
}
try {
logger.info('Using two-stage AI generation', {
jobId,
hasImages: !!referenceImages?.length,
narration: !!narration
})
const result = await generateTwoStageAIManimCode(
concept,
outputMode,
customApiConfig,
promptOverrides,
referenceImages,
() => ensureJobNotCancelled(jobId),
problemPlan,
async (stage) => {
await storeProcessingStage(jobId, stage, { status: 'processing' })
},
narration
? {
config: narration.config,
outputDirectory: narration.outputDirectory,
signal: narrationAbort.signal,
abort: () => narrationAbort.abort()
}
: undefined
)
if (result.code && result.code.length > 0) {
logger.info('Two-stage AI code generation succeeded', {
jobId,
length: result.code.length,
hasSceneDesign: !!result.sceneDesign,
hasNarration: !!result.narration
})
return {
code: result.code,
usedAI: true,
generationType: 'two-stage-ai',
sceneDesign: result.sceneDesign,
problemPlan,
referenceImages,
narration: result.narration
}
}
throw new Error('Two-stage AI returned an empty code result')
} catch (error) {
narrationAbort.abort()
if (narration) {
cleanupNarrationWorkspace(narration.outputDirectory)
}
// A cancelled TTS run must surface as JobCancelledError (with the stored reason), not as a
// generic narration failure that Bull might retry.
if (narration && await isJobCancelled(jobId)) {
await ensureJobNotCancelled(jobId)
}
logger.error('AI generation failed', { jobId, error: String(error) })
throw error
} finally {
if (cancellationWatcher) clearInterval(cancellationWatcher)
}
}
if (code) {
logger.info('Using pre-generated code', { jobId, length: code.length })
return { code, usedAI: false, generationType: analysisType, problemPlan, referenceImages }
}
throw new Error('No renderable code was generated')
}
/**
* 分析并生成(合并分析+生成)
*/
export async function analyzeAndGenerate(
jobId: string,
concept: string,
quality: string,
outputMode: OutputMode,
timings: Record<string, number>,
customApiConfig?: CustomApiConfig,
promptOverrides?: PromptOverrides,
referenceImages?: ReferenceImage[],
problemPlan?: ProblemFramingPlan,
narration?: NarrationStageRequest
): Promise<GenerationResult> {
const analyzeStart = performance.now()
const analysisResult = await analyzeConcept(jobId, concept, quality, outputMode)
timings.analyze = performance.now() - analyzeStart
const generateStart = performance.now()
try {
return await generateCode(
jobId,
concept,
quality,
outputMode,
analysisResult,
customApiConfig,
promptOverrides,
referenceImages,
problemPlan,
narration
)
} finally {
timings.generate = performance.now() - generateStart
}
}
|