代码实现细节
本文档针对项目中的关键代码模块进行深入剖析,阐述整体架构、核心服务、模块职责以及各模块之间的协作流程。
目录
项目概述
架构设计
核心模块说明
- ExplanationVideoService
- GeneratedVideoCacheService
- ManimPromptService
- CodeGenerateService 与 FixVideoCodeService
- PredictService
- LinuxService 与 Linux 端实现
数据库交互与缓存设计
流程详解
- 请求接收与权限校验
- 回答生成与缓存命中
- Manim 代码生成与执行
- 多场景循环生成与错误修复
- 视频合成与回调前端
- 封面图生成与异步更新
异常处理与重试机制
语音合成与同步策略
日志与监控
扩展与优化建议
1. 项目概述
本项目旨在基于用户输入的“主题”或“问题”,自动生成一段包含 Manim 可视化动画的视频,并将其通过 SSE(Server-Sent Events)流式推送给前端。同时,系统提供缓存命中逻辑,避免重复生成同样主题的视频。在动画生成过程中,结合 AI 聊天模型(如 Google Gemini),先生成回答文本,再生成对应的 Manim Python 脚本,最后将脚本提交至 Linux 服务器进行渲染并合成 HLS 视频分片。在所有视频片段合成完成后,再输出最终 MP4 链接。整个服务分为 Java 后端和 Linux 渲染端两部分,其中 Java 端负责编排、AI 调用、数据库交互和 SSE 推送,Linux 端负责实际的 Manim 渲染与 HLS 合成。
2. 架构设计
整体架构分为三层:
- 控制层(Controller / HTTP 接口):接收来自前端的 HTTP 请求,并通过 Tio-boot 框架将请求委托给内置的服务类。
- 业务层(Service 层):包括 ExplanationVideoService、GeneratedVideoCacheService、CodeGenerateService、PredictService、LinuxService 等核心服务,负责业务逻辑编排、AI 调用、Manim 脚本生成与执行、缓存查询与写入。
- 数据层(ActiveRecord 与数据库):使用 Java-db ActiveRecord 模式对 PostgreSQL(或其他关系型数据库)表进行插入、更新、查询,保存视频生成记录、LLM 使用日志、错误修复内容、缓存元数据等。
三层间通过 AOP(依赖注入)和 Row/Db 工具进行解耦。SSE 事件推送为异步流式模式,实时向前端更新生成进度。
同时引入了一台或多台独立的 Linux 渲染服务器,负责接收 Manim 脚本并执行渲染(调用本地 Manim Community Edition v0.19.0),再通过本地 NativeMedia C 库将分片合成 HLS 或最终 MP4。Java 端通过 LinuxClient 封装 HTTP 调用与 SSE 推送,完成跨服务器交互。
3. 核心模块说明
3.1 ExplanationVideoService
职责概述
作为视频生成的主流程入口。
校验用户身份、积分、黑名单,并通过 SSE 向前端积极反馈进度消息。
查询缓存:若已存在相同“主题+语言+声音”对应的视频记录,则直接通过 GeneratedVideoCacheService 返回缓存数据。
若未命中缓存:
- 在数据库中创建新的视频任务记录,包括 ef_ugvideo 与 ef_generated_video 两张表。
- 构建用户问题的聊天消息列表,先调用 AnswerService 生成文本回答并存入数据库。
- 根据 PromptEngine 渲染 Manim 代码生成提示,再调用 CodeGenerateService 获取第一段场景代码。
- 调用 LinuxService.startMainmSession 创建新的 HLS 会话,获取 sessionId 和基础 m3u8Path,并通过 SSE 通知前端。
- 执行第一段 Manim 代码,调用 runAndFixCode 进行循环运行与错误修复,获得第一个分片输出路径,加入 m3u8List。
- 进入循环生成后续场景:递增场景编号(senceNumber),直到收到 AI 返回的“done”标志或达到最大场景(默认为 5 个)。每次迭代先检查 code 是否为“done”,若不是则提交 LinuxService.runManimCode 渲染,出现异常则调用 FixVideoCodeService 修复代码;渲染成功后将片段路径加入 m3u8List,并继续生成下一场景提示。
- 所有场景渲染完成后,调用 finish 方法合并 HLS 片段为最终 MP4 并更新视频时长,向前端发送 metadata 和最终 URL,同时异步触发封面图生成任务。
主要方法
start(ExplanationVo, ChannelContext)
:入口,完成前置检查并调用 index。index(ExplanationVo, boolean, ChannelContext)
:核心编排流程,包括缓存检查、AI 回答生成、Manim 代码循环渲染。runAndFixCode(...)
:用于第一场景的循环执行与错误修复逻辑,最多尝试 10 次。finish(...)
:调用 LinuxService.finish 将 HLS 合并为 MP4,并返回最终视频长度与路径。
关键变量
locks
:用于并发场景提示缓存(genSence)时的分布式加锁。m3u8List
:存储各场景 HLS 片段路径,用于后续合并。messages
:与 AI 聊天交互的消息列表,保留所有对话历史,便于后续请求上下文传递。
3.2 GeneratedVideoCacheService
职责概述
- 负责根据“主题、语言、声音提供商、声音 ID”从持久化缓存(GeneratedVideoService 或 VideoBetterService)中检索已生成的视频记录。
- 若命中缓存,则快速为 ExplanationVideoService 返回缓存中的视频信息(封面地址、片段 URL、时长),并新建一条 ef_ugvideo 记录指向原有生成 ID,避免重复渲染。
- 实现缓存回退策略:先查询精准缓存,再查询“更好服务”推荐内容。
3.3 ManimPromptService
职责概述
- 查询或合并存储在数据库中的 Manim 代码生成规则与示例内容。
- 从表 ef_system_scence_promot 中读取系统级场景提示文本,供 CodeGenerateService 配置系统 Prompt。
- 组合多个示例文件(本地存储于资源目录),形成完整的 AI Prompt 示例部分。
- 避免“Bad Prompt”情况,通过 Aop 拦截器注入纠错提示或特定规则。
主要方法
retrieveManimCodeGenerationRuleFromDb(String topic)
:调用 AvoidErroneousPromptDataService 判断是否存在针对该 topic 的纠错提示并返回。getSystemPrompt(String language)
:读取表中所有未删除系统提示,按顺序拼接返回。v1()
:示例拼接逻辑,将若干本地示例文件内容追加到系统提示中。
3.4 CodeGenerateService 与 FixVideoCodeService
CodeGenerateService
职责概述
- 将 ExplanationVo 与 UniChatRequest 传入,调用 PredictService 与 UniRequestService 完成 AI 代码生成。
- 对 AI 返回的原始文本进行解析,提取出纯 Python 代码(调用 CodeUtils.parsePythonCode)。
- 将解析后的代码写入本地 scripts 目录,便于后续调试或持久化。
- 对用户修复请求(来自 FixVideoCodeService)调用 fixManaimCode,以获取修复后的 Python 代码。
主要流程
- 接受 UniChatRequest,其中 messages 已包含用户所有上下文与场景提示。
- 调用 Aop.get(UniRequestService).configRequest 配置请求参数(API Key、模型、温度等)。
- 设置系统 Prompt,温度为 0,调用 PredictService.generate 生成带注释的原始文本。
- 提取纯 Python 代码、写脚本并返回。
FixVideoCodeService
职责概述
- 在渲染阶段若某场景代码执行失败,则被调用以“修复”AI 返回的错误代码。
- 聚集当前错误场景信息(错误输出、标准输出、原始代码),拼接到一个新的 AI 请求消息列表中,附加“code_fix_prompt”提示。
- 调用 CodeGenerateService.fixManaimCode 获取修复后的代码,并返回给调用者。
- 异步保存本次修复前后对比及避免该错误的提示(调用 AvoidErroneousPromptDataService),便于未来“优先跳过”相似错误。
主要流程
- 构造新消息列表,依次添加:主题、场景编号、出错代码、stdout、stderr 以及修复提示模板。
- 调用 Config 配置请求、注入系统 Prompt,调用 PredictService.fixCode 获取修复文本。
- 解析修复代码并写入本地。若连续多次(>3)未修复,则生成下一场景提示,跳过本场景错误。
3.5 PredictService
职责概述
- 封装与外部 AI 聊天服务(如 Gemini、Claude)的交互与重试机制。
- 负责发送 UniChatRequest,并在 3 次重试内根据错误码(如 429、延迟提示)进行重试、全局或本次请求级别的限流。
- 解析 AI 返回结果,保存使用日志至数据库表 ef_llm_usage。
重试策略
- 共计 3 次尝试。
- 若遇到 429(Rate Limit),解析 AI 返回的 RetryInfo 延迟值,并记录到 ApiCooldownManager,全局后续请求均须等待该延迟。
- 若遇到 403(权限或余额不足),直接抛异常。
- 其他异常按固定后退延迟(30s)重试。
- 保存每次成功生成的使用日志(包含 groupId、taskId、模型、消息列表、使用量、耗时)。
3.6 LinuxService 与 Linux 端实现
LinuxService(Java 客户端)
职责概述
- 提供与独立 Linux 渲染服务器交互的封装,包括启动 HLS 会话、渲染 Manim 代码、合并 HLS 片段、合并 MP4。
- 对接 LinuxClient HTTP 调用,捕获异常并通过 SSE 向前端反馈错误消息。
主要接口
startMainmSession()
:调用 LinuxClient.startMainmSession,获取到 sessionIdPrt(HLS 会话标识)与 m3u8Path(HLS 播放列表初始路径)。finish(sessionIdPrt, m3u8Path, videos)
:调用 LinuxClient.finishMainmSession,传入所有已生成分片列表进行合并,返回合并后的视频时长。runManimCode(code, sessionIdPrt, m3u8Path, ChannelContext)
:循环调用 executeCode,将渲染日志通过 SSE 反馈,并重试 3 次。
Linux 端实现(ManimHanlder 与 ManimCodeExecuteService)
职责概述
- 以 HTTP 接口形式接收 Java 端发来的 HLS 会话启动请求、渲染请求、合并请求。
- 依赖 NativeMedia C 库完成 HLS 分片流式输出与最终 MP4 合并。
- ManimCodeExecuteService 实现具体的 Manim 脚本写入、调用 Manim CLI 渲染、日志收集、片段上传到 HLS。
主要流程
start
接口:- 在服务器本地创建唯一子目录(./data/session/{sessionId}),并初始化 HLS 播放列表与分片文件名模式(调用 NativeMedia.initPersistentHls)。
- 返回 sessionId 与 sessionIdPrt(NativeMedia 内部会话句柄)及 m3u8 路径。
index
接口:- 以 SSE 模式接收 Manim 代码字符串,调用 ManimCodeExecuteService.executeCode 执行脚本。
- 在执行前将脚本写入 cache 目录,拷贝 util 库文件;然后执行 Manim CLI 渲染,渲染结果(CombinedScene.mp4)写入指定子目录。
- 将渲染生成的 MP4 或 HLS 分片路径通过 JSON 返回给 Java 端。
finish
接口:- 接收 sessionIdPrt、m3u8Path、videos 列表,若子目录存在则先调用 NativeMedia.finishPersistentHls 结束 HLS 会话。
- 根据视频片段列表合并 MP4,调用 NativeMedia.merge,并通过 NativeMedia.getVideoLength 获取总时长。
- 返回合并完成后的总时长。
4. 数据库交互与缓存设计
4.1 数据表概览
- ef_ugvideo:用户视频请求表,记录每次发起请求时的唯一 ID、group_id(缓存关联)、主题、语言、声音提供商、声音 ID、最终视频 URL、视频时长、耗时等信息。
- ef_generated_video:持久化生成视频记录,保存原始生成时的 ID、group_id、MD5(主题摘要)、语言、声音提供商、声音 ID、AI 回答、封面 URL、视频 URL、视频时长等。该表用于缓存查找与重复请求命中。
- ef_manin_sence_code:Manim 每个场景代码执行记录表,保存代码脚本、stdout、stderr、执行状态(成功/失败)、输出分片 URI、循环次数、耗时等。用于调试与错误追踪。
- ef_llm_usage:AI 使用日志表,记录每次与 LLM 的交互内容,包括 groupId、taskId、模型、消息列表、消耗 tokens、时间耗时等。用于耗费统计与审计。
- ef_generate_sence:AI 生成场景提示缓存表,按 MD5 存储“场景提示”文本,避免重复调用 genSence。
- ef_generate_code_avoid_error_prompt:存储在错误修复过程中生成的“避免此类错误”的提示,用于后续提示优先避免相同错误。
- 其他辅助表:包括 ef_system_scence_promot、ef_generate_sence、ef_generate_code_avoid_error_prompt、用户黑名单、用户积分等。
4.2 缓存逻辑
首次请求
- 计算主题 MD5,插入 ef_ugvideo 与 ef_generated_video 两条新记录。
- 调用 getByTopic 检查 ef_generated_video 是否已存在相同主题及配置,若不存在,则走完整生成流程;若存在则进入缓存分支。
缓存分支
- GeneratedVideoCacheService.retrieveByTopic 首先查询 ef_generated_video 表中相同 MD5、语言、声音提供商、声音 ID 的记录。
- 若命中,则将该记录转译为 GeneratedVideo 对象返回,不再重新渲染;同时向 ef_ugvideo 新插入一条记录,group_id 指向旧记录的 ID。
AI 场景提示缓存
- genSence 方法生成某一场景或子主题的 AI 提示并存入 ef_generate_sence 表,下次相同 MD5 调用可直接读取。
错误修复提示缓存
- FixVideoCodeService.generateErrorReasonAndSave 在每次错误修复后,将“避免相同错误的提示”写入 ef_generate_code_avoid_error_prompt。若后续出现类似错误,可优先从该表获取提示注入 AI 请求。
5. 流程详解
5.1 请求接收与权限校验
- 前端通过 HTTP POST 将请求(包含 prompt、language、voice_provider、voice_id、user_id、stream 等信息)发送到相应接口。
- Backend Controller(基于 Tio)将请求封装为 ExplanationVo 对象,并通过 ChannelContext 保存 SSE 通道。
- ExplanationVideoService.start 方法首先验证 user_id 是否存在、是否在黑名单、积分是否足够(调用 UserBlockedService、UserCreditService)。
- 若校验不通过,通过 SSE 返回对应错误码(400/403/401),并在 finally 块中发送“close”事件关闭 SSE。
5.2 回答生成与缓存命中
- 在 start 方法中调用 index,首先构造主题 MD5 并调用 GeneratedVideoCacheService.retrieveByTopic 判断缓存。
- 若缓存命中,直接通过 returnFromCache 方法发送 metadata、最终 URL 并返回 ExplanationResult。
- 若缓存未命中:在 ef_ugvideo 与 ef_generated_video 插入新记录,初始字段(group_id、title、language、user_id、voice_id、md5、topic)。
- 通过 PromptEngine 渲染“生成回答”场景提示(user_topic_prompt.txt),构建 ChatMessage 列表并调用 AnswerService.genAnswer 获取回答。
- 将回答结果写入 ef_generated_video.answer 字段,并通过 SSE 推送“answer”消息给前端。
5.3 Manim 代码生成与执行
构建 UniChatRequest,并设置组 ID(groupId = id)、组名称(topic)、任务 ID=1、任务名称=sence 1、Provider。
调用 CodeGenerateService.genManaimCode,内部首先注入系统 Prompt(ManimPromptService.getSystemPrompt),再调用 PredictService.generate 生成带注释的原始文本,提取 Python 代码并写本地脚本。
通过 LinuxService.startMainmSession 与 LinuxClient.startMainmSession 建立 HLS 会话,记录 sessionIdPrt、m3u8Path,并通过 SSE 推送“task”消息给前端。
调用 runAndFixCode 进行第一场景循环渲染:
- 循环最多 10 次,调用 LinuxService.runManimCode 执行 Manim 脚本,若失败则记录 stdErr/stdOut,将此次脚本、日志插入 ef_manin_sence_code 表,并调用 FixVideoCodeService 修复代码,继续尝试;
- 若成功生成分片(HLS 输出),记录 stdout/stdErr、uri,并插入 ef_manin_sence_code。返回最终“可执行”代码用于后续 messages 列表追加。
- 通过 SSE 推送“progress”消息告知前端当前进度。
第一场景渲染完成后,将分片路径加入 m3u8List,并通过 SSE 推送“finish first scene code”消息给前端。
5.4 多场景循环生成与错误修复
循环场景编号从 2 到最多 5(或直到 AI 返回“done”):
- 先渲染提示“start generate second sence code”,构造 next_sence_prompt(PromptEngine 渲染 generate_next_sence_prompt.txt,传入 senceNumber 参数),并将其作为用户消息添加到 messages 列表中;
- 设置 UniChatRequest 的 taskId = senceNumber,taskName = “sence N”,并调用 CodeGenerateService.genManaimCode 获取本场景代码;
- 若 code 为 null 或为空,则通过 SSE 推送错误并退出循环;
- 若 code 内容包含“done”关键字或 senceNumber > 10,则跳出循环;
- 否则调用 LinuxService.runManimCode 执行 Manim 渲染,若失败(输出为空),记录错误日志插入 ef_manin_sence_code,累加错误计数并调用 FixVideoCodeService 修复代码;
- 若成功,加入 m3u8List,并通过 SSE 推送“finish N scene code”消息;然后递增 senceNumber,继续下一次循环。
在多场景循环过程中,每次出现错误时最多允许 5 次连续修复尝试,超过后直接跳转到下一场景。若所有场景渲染完成或返回“done”,进入 finish 步骤。
5.5 视频合成与回调前端
- 循环结束后,调用 LinuxService.finish 合并 HLS 片段:将 m3u8List(以逗号分隔)传入后端 finish 接口,合并为最终 MP4,返回视频时长。
- 根据返回视频时长计算整数值并更新 ef_generated_video 及 ef_ugvideo 表中的 video_length 字段。
- 生成最终 MP4 URL(将 m3u8Path 替换后缀为 .mp4),通过 SSE 推送“metadata”与“main”事件给前端。
- ExplanationResult 中携带最终 id、m3u8Path、视频时长等信息返回给调用方。
5.6 封面图生成与异步更新
在 ExplanationVideoService 返回 ExplanationResult 后,通过 TioThreadUtils.submit 异步任务生成封面图:
- 构造 UniChatRequest (仅包含 id 与 topic),调用 ManimImageService.index,生成一张静态封面图并返回 URL;
- 分别更新 ef_generated_video.cover_url 与 ef_ugvideo.cover_url 字段。
由于该操作不影响实时视频生成流程,采用异步线程执行,若发生错误则仅打印 StackTrace。
6. 异常处理与重试机制
用户校验失败:直接通过 SSE 返回对应状态码(400、403、401),并关闭 SSE 连接。
AI 调用失败:PredictService 在遇到各类异常时按策略重试;若重试完成仍未成功,则抛出异常终止流水,并通过 SSE 推送错误。
Manim 渲染超时或错误:
- LinuxCodeExecuteService 对每个脚本执行等待超时时间 120s,超时则强制销毁进程并返回 exitCode = -1。
- ExplanationVideoService 收到空输出时认为渲染失败,记录错误并触发 FixVideoCodeService 修复;若修复失败次数过多,则跳过此场景。
HLS 合并失败:在 finish 阶段若 m3u8Path 对应文件不存在或合并失败,将跳过 MP4 生成,只调用 freeHlsSession 并记录日志。
7. 语音合成与同步策略
利用
manim_utils.custom_voiceover_tts
方法进行 TTS,语音文件缓存至本地并产出时长。在每个场景的构造函数中使用
with custom_voiceover_tts(voice_text) as tracker:
语法:首先调用
self.add_sound(tracker.audio_path)
将音频加入 Manim 时间轴。根据音频时长与各步动画的 run_time 之和,计算剩余等待时间:
wait_time = max(1, tracker.duration - 已用动画时长) self.wait(wait_time)
确保可视动画与语音同步,同时留出至少 1s 的缓冲。
当语音 TTS 失败(tracker.audio_path 为 null 或时长 ≤ 0),打印警告并在视觉上仅播放动画。
8. 日志与监控
日志记录:
- 使用 Lombok 的
@Slf4j
输出info
、warn
、error
等日志,跟踪关键节点(如 “start generate answer”、“finish sence N code”)。 - 在 PredictService 中对每次 AI 调用、失败重试、延迟提示等关键步骤进行日志记录。
- 在 Linux 端通过 ManimHanlder、ManimCodeExecuteService 记录脚本执行过程、O‐S 错误与渲染结果。
- 使用 Lombok 的
错误告警:
- 在 PredictService 捕获
GenerateException
时,通过 AlarmUtils.sendException 将告警发送至监控系统(可集成钉钉、Slack 或邮件)。 - 在 Manim 渲染失败、HLS 合并失败时,打印错误并通过日志集中上报。
- 在 PredictService 捕获
数据库监控:
- ef_llm_usage 表可用于统计 AI 模型调用量、失败率、耗时等。
- ef_manin_sence_code 表用于分析在哪一场景、哪段代码出现错误、修复次数分布及失败率。
9. 扩展与优化建议
并发性能优化
- 目前采用 Striped 锁+数据库 MD5 缓存判断,可进一步引入分布式缓存(如 Redis)提高缓存查询速度,并在高并发场景下减少数据库压力。
- 对 Manim 渲染请求可进行并发队列化管理,比如使用消息队列(Kafka、RabbitMQ)分发给多台 Linux 渲染实例。
异常修复智能化
- 当前错误修复逻辑基于 AI 重新生成,下次出现同样错误时仅跳过;可考虑在 ef_generate_code_avoid_error_prompt 表中存储更细粒度的错误原因与修复策略,以便下次自适应绕过或自动修正。
- 引入静态代码检查(如运行 pytest 或 flake8)在提交 Manim 脚本前进行预验证,减少 AI 生成代码不可执行情况。
视频合成优化
- HLS 合并为 MP4 阶段可并行下载各片段并在本地完成合并,减少 I/O 串行开销。
- 利用 FFmpeg 的 concat 协议直接合并视频片段,而非先转成 HLS 再合并 MP4,节省一部分冗余转码时间。
多语言与模型支持扩展
- 当前系统支持英文与中文提示,可扩展为更多语言。ManimPromptService 可按语言动态加载不同本地示例文件。
- AI 模型可替换为更高版本(如 Gemini+),并通过 PredictService 配置不同 Provider 的 API Key 或参数,支持多模型并行调用。
UI 体验优化
- SSE 推送的消息中仅包含简单键值对,可考虑加入更丰富的结构(如 JSON 指定当前进度百分比、已完成场景数量),便于前端可视化进度条。
- 支持预览功能:在第一场景渲染成功后即可返回第一段视频片段 URL,前端可以先行播放。
安全与权限控制
- 当前仅校验 user_id 和积分,可进一步添加频率限制(每用户每分钟请求次数)、IP 白名单/黑名单、接口身份验证等。
- AI Key、Linux 服务地址等敏感配置应当放置于安全配置中心或加密存储,避免硬编码。
结语
本文详细介绍了项目整体架构、核心模块职责、业务流程以及异常处理与优化思路。通过分层设计、模块解耦、AI 与 Manim 渲染协作,实现了从“文本到动画视频”的完整自动化流水线。在未来,可结合多模型、多实例并行渲染以及缓存策略优化,进一步提升系统稳定性与性能。