Manim 图像生成服务客户端文档
一、简介
随着 AI 与可视化技术的结合,自动化生成教学或演示用的动画图像变得至关重要。本项目基于 Manim(一个 Python 动画引擎)与 Google Gemini 模型,通过 Java 服务端控制代码生成、渲染与错误修复,最终生成可用于前端展示的图像。
本文档将带领读者了解整个 Manim 图像生成客户端的实现,包括:
- 测试用例的配置与调用
- 核心服务
ManimImageService
的设计与实现 - 辅助服务
LinuxService
的接口与重试机制 - 工作流程与关键代码详解
- 输出示例
本文档力求详细且不遗漏任何代码,方便读者快速上手与二次开发。
二、依赖与环境
- Java 版本:JDK 17+
- 框架与库:
- JFinal AOP
- Tio-Boot(用于 SSE 推送与测试)
- com.jfinal.kit.Kv
- Google Gemini Java 客户端
- 自研工具类:
PromptEngine
、LinuxClient
、FastJson2Utils
等
- 外部服务:
- Gemini 2.5 Pro 模型(用于生成 Manim Python 代码)
- 后端 Manim 渲染接口(由
LinuxClient
调用)
在开始之前,请确保:
- 已配置好 Gemini 客户端的 API Key 与模型版本。
- Manim 渲染服务器地址与访问密钥配置正确。
- 本地或 CI 环境能够访问上述外网服务。
三、测试用例(ManimImageServiceTest
)
package com.litongjava.manim.services;
import org.junit.Test;
import com.litongjava.tio.utils.environment.EnvUtils;
public class ManimImageServiceTest {
@Test
public void test() {
// 加载环境变量(如 API Key、服务器地址)
EnvUtils.load();
// 调用服务生成图像,参数:主题、语言、可选 ChannelContext
String url = Aop.get(ManimImageService.class)
.index("How Does ChatGPT Work?", "English", null);
System.out.println(url);
}
}
说明:
EnvUtils.load()
将读取配置文件(如application.properties
)中的 API Key、服务器地址等。Aop.get(ManimImageService.class)
通过 JFinal AOP 获取服务实例。index(...)
方法执行后,返回最终生成的图像 URL。
四、核心服务:ManimImageService
package com.litongjava.manim.services;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.jfinal.kit.Kv;
import com.litongjava.gemini.GeminiChatRequestVo;
import com.litongjava.gemini.GeminiChatResponseVo;
import com.litongjava.gemini.GeminiClient;
import com.litongjava.gemini.GeminiGenerationConfigVo;
import com.litongjava.gemini.GoogleGeminiModels;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.linux.ProcessResult;
import com.litongjava.openai.chat.ChatMessage;
import com.litongjava.template.PromptEngine;
import com.litongjava.tio.core.ChannelContext;
import com.litongjava.tio.core.Tio;
import com.litongjava.tio.http.common.sse.SsePacket;
import com.litongjava.tio.utils.hutool.FileUtil;
import com.litongjava.tio.utils.hutool.ResourceUtil;
import com.litongjava.tio.utils.json.FastJson2Utils;
import com.litongjava.tio.utils.json.JsonUtils;
import com.litongjava.tio.utils.snowflake.SnowflakeIdUtils;
import com.litongjava.vo.ToolVo;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ManimImageService {
private static final boolean debug = false;
private LinuxService linuxService = Aop.get(LinuxService.class);
/**
* 入口方法:生成 Manim 图像
* @param topic 图像主题
* @param language 语言
* @param channelContext SSE 通道上下文,可用于推送生成进度
* @return 最终图像 URL
*/
public String index(String topic, String language, ChannelContext channelContext) {
// 1. 读取系统提示(System Prompt)
String systemPrompt = FileUtil
.readURLAsString(ResourceUtil.getResource(
"prompts/generate_manim_image_code_system_prompt.txt"))
.toString();
// 2. 渲染用户 Prompt
Kv kv = Kv.by("topic", topic).set("language", language);
String prompt = PromptEngine
.renderToString("manim_image_code_prompt.txt", kv);
// 3. 构建 ChatMessage 列表
ChatMessage chatMessage = new ChatMessage("user", prompt);
List<ChatMessage> messages = new ArrayList<>();
messages.add(chatMessage);
// 4. 配置 Gemini 请求
GeminiChatRequestVo geminiChatRequestVo = new GeminiChatRequestVo();
GeminiGenerationConfigVo config = new GeminiGenerationConfigVo();
config.setTemperature(0d);
geminiChatRequestVo.setGenerationConfig(config);
geminiChatRequestVo.setSystemPrompt(systemPrompt);
geminiChatRequestVo.setChatMessages(messages);
// 5. 首次生成代码
String code = genCdoe(geminiChatRequestVo);
messages.add(new ChatMessage("model", code));
if (code == null) {
return null;
}
// 6. 调用 Linux 服务渲染图像
ProcessResult processResult = linuxService.runManimImageCode(code, channelContext);
// 7. 错误重试:最多 10 次
for (int i = 0; i < 10; i++) {
if (processResult != null && processResult.getOutput() != null) {
return processResult.getOutput();
} else {
// 根据 stderr 修复代码并重试
code = this.fixCode(i, processResult.getStdErr(), messages, geminiChatRequestVo, channelContext);
messages.add(new ChatMessage("model", code));
processResult = linuxService.runManimImageCode(code, channelContext);
}
}
return null;
}
// 修复生成失败的代码
private String fixCode(int attempt, String stdErr, List<ChatMessage> messages,
GeminiChatRequestVo request, ChannelContext channelContext) {
String info = "start fix manim image code " + attempt;
log.info(info);
if (channelContext != null) {
byte[] bytes = FastJson2Utils.toJSONBytes(Kv.by("info", info));
Tio.bSend(channelContext, new SsePacket("progress", bytes));
}
// 构建修复提示
String codeFixPrompt = PromptEngine
.renderToString("code_fix_prompt.txt", Kv.by("error", stdErr));
messages.add(new ChatMessage("user", codeFixPrompt));
request.setChatMessages(messages);
return this.genCdoe(request);
}
// 调用 Gemini 生成代码并抽取 Python 段
private String genCdoe(GeminiChatRequestVo vo) {
GeminiChatResponseVo resp;
try {
resp = GeminiClient.generate(
GoogleGeminiModels.GEMINI_2_5_PRO_PREVIEW_03_25, vo);
} catch (Exception e) {
log.error("Failed to generate code: {}", JsonUtils.toJson(vo), e);
return null;
}
String text = resp.getCandidates().get(0)
.getContent().getParts().get(0).getText();
// 提取 ```python ... ``` 或 JSON 格式中的 code 字段
int start = text.indexOf("```python");
if (start == -1) {
// 处理非 markdown 格式
if (text.startsWith("# -*- coding: utf-8 -*-")) {
return text;
}
log.error("No code found in output: {}", text);
return null;
}
int end = text.lastIndexOf("```", text.length());
String code = text.substring(start + 9, end > start ? end : text.length())
.trim();
// 将代码写入本地文件(可选)
try {
new File("script").mkdirs();
String path = "script/" + SnowflakeIdUtils.id() + ".py";
FileUtil.writeString(code, path, "UTF-8");
} catch (IOException ignored) {}
return code;
}
}
功能与流程:
- 系统 Prompt:读取预先编写的系统提示,告诉模型如何生成 Manim Python 代码。
- 用户 Prompt:根据主题和语言动态渲染,形成对话上下文。
- 生成代码:通过
genCdoe
方法调用 Gemini,抽取出 Python 代码段。 - 执行与重试:首次调用
runManimImageCode
渲染,若出现错误则调用fixCode
进行错误修复,最多重试 10 次。 - 进度推送:若提供了 SSE 通道,则在各步骤发送进度或错误信息,方便前端展示。
五、辅助服务:LinuxService
package com.litongjava.manim.services;
import com.jfinal.kit.Kv;
import com.litongjava.linux.LinuxClient;
import com.litongjava.linux.ProcessResult;
import com.litongjava.tio.core.ChannelContext;
import com.litongjava.tio.core.Tio;
import com.litongjava.tio.http.common.sse.SsePacket;
import com.litongjava.tio.utils.json.FastJson2Utils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LinuxService {
private static final String apiBase = "http://13.216.69.13";
private static final String api_key = "123456";
public static final boolean debug = false;
// 调用 Manim 图像接口,最多重试 3 次
public ProcessResult runManimImageCode(String code, ChannelContext channelContext) {
ProcessResult result = null;
for (int i = 0; i < 3; i++) {
try {
result = LinuxClient.manimImage(apiBase, api_key, code);
break;
} catch (Exception e) {
String msg = "count:" + i + " " + e.getMessage();
log.error(msg, e);
if (channelContext != null) {
byte[] bytes = FastJson2Utils.toJSONBytes(Kv.by("info", msg));
Tio.bSend(channelContext, new SsePacket("progress", bytes));
}
}
}
return result;
}
}
说明:
manimImage
方法负责将 Python 代码发送至远端 Manim 渲染服务。- 重试机制:遇到网络或服务异常时,最多尝试 3 次。
- SSE 推送:每次异常时,向前端发送进度日志。
六、工作流程示意
- 测试启动:JUnit 测试触发
ManimImageService.index
。 - Prompt 构建:组装 system prompt 与用户 prompt。
- 代码生成:调用 Gemini 模型,获取 Manim Python 脚本。
- 本地保存:可选地将脚本写入
script/
目录以便调试。 - 渲染请求:调用
LinuxService.runManimImageCode
,推送 SSE 进度。 - 错误修复:若渲染失败,调用
fixCode
重新生成代码并重试。 - 结果返回:获取渲染输出(图片 URL),作为方法返回值。
七、示例输出
/media/images/501229158053711872/Main_ManimCE_v0.19.0.png
此 URL 可用于在前端或其他系统中直接访问生成的图像。
八、扩展与优化建议
- 并发控制:当前服务无并发限流,建议结合线程池或队列进行控制。
- 模型多样性:可支持多种 Gemini 模型版本,通过配置动态切换。
- 错误分析:将
stdErr
中的关键信息持久化,以便统计常见错误类型。 - 超时管理:为渲染请求添加超时控制,避免长时间阻塞。