Java 调用 manim 命令 执行代码 生成封面
1. 项目背景与整体思路
本项目主要用于接收客户端提交的 Manim 脚本代码,通过 Java 程序调用 Manim 渲染工具执行该脚本,生成视频封面图片。整体流程如下:
请求接收
HTTP 请求发送包含 Manim 脚本代码到后端。后端控制器(Handler)接收请求,并调用服务层处理代码执行任务。代码保存与执行
服务层将接收到的脚本代码保存为 Python 文件,并使用manim
命令调用 Manim 渲染器执行该脚本。执行过程中同时将命令行的标准输出与错误输出重定向至日志文件,以便后续调试。结果返回
执行结束后,将日志信息和生成的封面图片的路径封装成 JSON 对象返回给客户端。
2. 核心代码说明
2.1. ManimImageHandler 类
该类作为 HTTP 请求的入口,主要功能包括:
- 接收请求:通过
request.getBodyString()
获取客户端提交的 Python 脚本代码。 - 调用服务层:调用
ManimImageService.executeCode(code)
方法处理代码的保存和执行任务。 - 返回结果:将执行结果(包括标准输出、错误输出、任务编号以及图片路径)以 JSON 格式返回,并支持跨域访问(CORS)。
下面是完整代码:
package com.litongjava.linux.handler;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.linux.ProcessResult;
import com.litongjava.linux.service.ManimImageService;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.http.server.util.CORSUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ManimImageHandler {
ManimImageService manimService = Aop.get(ManimImageService.class);
public HttpResponse index(HttpRequest request) {
HttpResponse response = TioRequestContext.getResponse();
CORSUtils.enableCORS(response);
String code = request.getBodyString();
try {
ProcessResult executeScript = manimService.executeCode(code);
if (executeScript != null) {
response.setJson(executeScript);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
response.setStatus(500);
response.setString(e.getMessage());
}
return response;
}
}
说明:
TioRequestContext.getResponse()
获取当前请求的响应对象;CORSUtils.enableCORS(response)
用于设置跨域访问,确保前端可跨域调用;- 获取请求体中的代码后,调用服务层的
executeCode
方法执行代码,并将结果包装成 JSON 返回。
2.2. ManimImageService 类
该类主要负责以下工作:
文件保存
- 根据生成的唯一任务 ID(通过雪花算法获得)在
scripts/
目录下创建一个文件夹; - 将接收到的 Python 脚本代码写入到以任务 ID 命名的
.py
文件中。
- 根据生成的唯一任务 ID(通过雪花算法获得)在
脚本执行
- 利用
ProcessBuilder
调用manim -s -qh --format=png
命令执行脚本; - 设置环境变量
PYTHONIOENCODING
为utf-8
确保输出编码正确; - 将执行过程的标准输出和错误输出分别重定向到
stdout.log
和stderr.log
文件。
- 利用
结果处理
- 读取日志文件内容,封装到
ProcessResult
对象中; - 根据执行脚本生成的图片所在目录(如
media/images/{任务ID}
)遍历查找 PNG 图片,并设置图片的路径。
- 读取日志文件内容,封装到
下面是完整代码:
package com.litongjava.linux.service;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import com.litongjava.linux.ProcessResult;
import com.litongjava.tio.utils.hutool.FileUtil;
import com.litongjava.tio.utils.snowflake.SnowflakeIdUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ManimImageService {
public ProcessResult executeCode(String code) throws IOException, InterruptedException {
long id = SnowflakeIdUtils.id();
String folder = "scripts" + File.separator + id;
File fileFolder = new File(folder);
if (!fileFolder.exists()) {
fileFolder.mkdirs();
}
String scriptPath = folder + File.separator + id + ".py";
FileUtil.writeString(code, scriptPath, StandardCharsets.UTF_8.toString());
String imageFolder = "media" + File.separator + "images" + File.separator + id;
log.info("imageFolder:{}", imageFolder);
// 执行脚本
ProcessResult execute = execute(scriptPath);
execute.setTaskId(id);
File imageFolderFile = new File(imageFolder);
File[] listFiles = imageFolderFile.listFiles();
log.info("listFiles length:{}", listFiles.length);
for (File file : listFiles) {
if (file.getName().endsWith(".png")) {
String name = file.getName();
String filePath = imageFolder + File.separator + name;
String newFilePath = filePath.replace("\\", "/");
execute.setOutput("/" + newFilePath);
}
}
return execute;
}
public static ProcessResult execute(String scriptPath) throws IOException, InterruptedException {
String osName = System.getProperty("os.name").toLowerCase();
log.info("osName: {} scriptPath: {}", osName, scriptPath);
ProcessBuilder pb = new ProcessBuilder("manim", "-s", "-qh", "--format=png", scriptPath);
pb.environment().put("PYTHONIOENCODING", "utf-8");
// 获取脚本所在目录
File scriptFile = new File(scriptPath);
File scriptDir = scriptFile.getParentFile();
if (scriptDir != null && !scriptDir.exists()) {
scriptDir.mkdirs();
}
// 定义日志文件路径,存放在与 scriptPath 相同的目录
File stdoutFile = new File(scriptDir, "stdout.log");
File stderrFile = new File(scriptDir, "stderr.log");
// 将输出和错误流重定向到对应的日志文件
pb.redirectOutput(stdoutFile);
pb.redirectError(stderrFile);
Process process = pb.start();
int exitCode = process.waitFor();
// 读取日志文件内容,返回给客户端(如果需要实时返回,可用其他方案监控文件变化)
String stdoutContent = new String(Files.readAllBytes(stdoutFile.toPath()), StandardCharsets.UTF_8);
String stderrContent = new String(Files.readAllBytes(stderrFile.toPath()), StandardCharsets.UTF_8);
ProcessResult result = new ProcessResult();
result.setExitCode(exitCode);
result.setStdOut(stdoutContent);
result.setStdErr(stderrContent);
return result;
}
}
说明:
- 任务 ID 生成:使用
SnowflakeIdUtils.id()
生成全局唯一 ID,确保每个任务的脚本和生成的图片存放在独立目录中。 - 文件操作:通过
FileUtil.writeString
将接收到的代码写入文件;确保相关目录存在,否则创建目录。 - ProcessBuilder 配置:
- 设置执行命令为
manim -s -qh --format=png scriptPath
; - 配置环境变量
PYTHONIOENCODING
为utf-8
,避免输出编码问题; - 将标准输出和错误输出分别重定向到日志文件
stdout.log
和stderr.log
,方便后续排查问题。
- 设置执行命令为
- 图片查找与路径设置:在
media/images/{任务ID}
目录下查找以.png
后缀的文件,并设置路径到ProcessResult
对象中,以便客户端可以直接访问生成的封面图片。
2.3. 执行结果示例
执行脚本完成后,最终返回的 JSON 格式结果如下所示,其中 output
字段即为生成封面图片的相对路径:
{
"output": "/media/images/501219237140631552/Main_ManimCE_v0.19.0.png"
}
3. 总结
本文档详细阐述了如何在 Java 项目中调用 Manim 命令执行 Python 代码,生成视频封面。整个流程包括:
- 前端请求中提交 Manim 脚本代码;
- 后端 Handler 接收请求并调用 Service 执行代码;
- Service 层将代码写入文件、执行 Manim 命令,并将生成的 PNG 图片路径返回;
- 通过日志文件记录标准输出与错误输出,方便问题排查。
所有代码均未省略,并且每个部分均附有详细的说明和解释,方便读者理解各个环节的作用及实现原理。