Skip to content

执行 Python (Manim) 代码 ​

本文档介绍了基于 java-kit-server 项目环境下如何通过 Java 程序执行 Python 代码(主要用于执行 Manim 脚本)的实现方案。文档包括整体架构、工作流程说明以及完整的代码实现,确保不遗漏您提供的任何代码。


1. 项目概述 ​

该项目旨在提供一个基于 Java 后端的 Python 代码执行服务。用户通过 HTTP 请求提交包含 Manim 脚本的代码,系统将自动执行脚本并返回执行结果。项目主要包含两个模块:

  • 请求处理层(ManimHandler)
    负责接收 HTTP 请求、提取代码内容,并调用业务逻辑处理后返回 JSON 格式的执行结果。

  • 业务逻辑层(ManimService)
    负责代码预处理(目录创建、占位符替换)、将代码保存为脚本文件,启动 Python 进程执行脚本,捕获输出日志,并检查生成的 Manim 视频文件。


2. 工作流程与架构 ​

2.1 整体流程 ​

  1. 接收请求
    用户通过 HTTP 请求提交 Manim 代码,ManimHandler 的 index 方法接收请求,并将代码内容记录到日志中。

  2. 代码预处理与文件存储
    ManimService.executeCode(String code) 方法中:

    • 创建用于缓存执行结果的目录(如 cache)。
    • 利用 Snowflake 算法生成唯一任务 ID,并在 cache 和 scripts 目录下分别创建对应的子目录。
    • 替换代码中的占位符 #(output_path) 为生成的输出路径。
    • 将代码写入生成的 Python 脚本文件中。
  3. 执行 Python 脚本
    使用 ProcessBuilder 构造适合当前操作系统的命令(Windows 下调用 python,Linux/Unix 下调用 python3),启动 Python 进程执行脚本,并捕获标准输出和错误输出。

  4. 返回执行结果
    收集执行结果(标准输出、错误输出、退出码),检查生成的视频文件(路径为 cache/{taskId}/videos/1080p30/CombinedScene.mp4),将结果封装到 ProcessResult 对象中,并通过 HTTP 响应返回给客户端。

2.2 异常处理 ​

  • 在请求处理层和业务逻辑层均采用异常捕获机制,确保在出现异常时:
    • 日志中记录详细错误信息,便于问题排查;
    • HTTP 响应返回状态码 500,并附带错误信息。

3. 完整代码 ​

3.1 ManimHandler.java ​

java
package nexus.io.linux.handler;

import nexus.io.jfinal.aop.Aop;
import nexus.io.linux.ProcessResult;
import nexus.io.linux.service.ManimService;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ManimHandler {
  ManimService manimService = Aop.get(ManimService.class);

  public HttpResponse index(HttpRequest request) {
    String code = request.getBodyString();
    log.info("code:{}", code);
    HttpResponse response = TioRequestContext.getResponse();
    try {
      ProcessResult executeScript = manimService.executeCode(code);
      executeScript.setExecuteCode(code);
      response.setJson(executeScript);
    } catch (Exception e) {
      log.error(e.getMessage(), e);
      response.setStatus(500);
      response.setString(e.getMessage());
    }
    return response;
  }
}

3.2 ManimService.java ​

java
package nexus.io.linux.service;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

import com.jfinal.kit.Kv;
import com.jfinal.template.Engine;
import com.jfinal.template.Template;
import nexus.io.linux.ProcessResult;
import nexus.io.tio.utils.hutool.FileUtil;
import nexus.io.tio.utils.snowflake.SnowflakeIdUtils;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ManimService {

  public ProcessResult executeCode(String code) throws IOException, InterruptedException {
    new File("cache").mkdirs();
    long id = SnowflakeIdUtils.id();
    String subFolder = "cache" + File.separator + id;
    code = code.replace("#(output_path)", subFolder);
    //Template template = Engine.use().getTemplateByString(code);
    //code = template.renderToString(Kv.by("output_path", subFolder));

    String folder = "scripts" + File.separator + id;
    File fileFolder = new File(folder);
    if (!fileFolder.exists()) {
      fileFolder.mkdirs();
    }
    String scriptPath = folder + File.separator + "script.py";
    FileUtil.writeString(code, scriptPath, StandardCharsets.UTF_8.toString());
    ProcessResult execute = execute(scriptPath);
    execute.setTaskId(id);
    String filePath = subFolder + File.separator + "videos" + File.separator + "1080p30" + File.separator + "CombinedScene.mp4";
    File file = new File(filePath);
    if (file.exists()) {
      execute.setOutput(filePath.replace("\\", "/"));
    } else {
      log.info("file is not exists:{}", filePath);
    }
    return execute;
  }

  public static ProcessResult execute(String scriptPath) throws IOException, InterruptedException {
    // 构造 ProcessBuilder
    String osName = System.getProperty("os.name");
    ProcessBuilder pb = null;
    if (osName.toLowerCase().contains("windows")) {
      pb = new ProcessBuilder("python", scriptPath);
    } else {
      pb = new ProcessBuilder("python3", scriptPath);
    }
    pb.environment().put("PYTHONIOENCODING", "utf-8");

    Process process = pb.start();

    // 读取标准输出 (可能包含base64以及脚本本身的print信息)
    BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));

    // 读取错误输出
    BufferedReader stdError = new BufferedReader(new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8));

    // 用于存放所有的标准输出行
    StringBuilder outputBuilder = new StringBuilder();

    String line;
    while ((line = stdInput.readLine()) != null) {
      outputBuilder.append(line).append("\n");
    }

    // 收集错误输出
    StringBuilder errorBuilder = new StringBuilder();
    while ((line = stdError.readLine()) != null) {
      errorBuilder.append(line).append("\n");
    }

    // 等待进程结束
    int exitCode = process.waitFor();

    // 构造返回实体
    ProcessResult result = new ProcessResult();
    result.setExitCode(exitCode);
    result.setStdOut(outputBuilder.toString());
    result.setStdErr(errorBuilder.toString());
    return result;
  }

}

4. 使用说明 ​

4.1 代码提交与执行流程 ​

  1. 提交代码
    用户通过 HTTP POST 请求将包含 Manim 脚本的代码提交至系统,请求体中包含待执行的代码内容。

  2. 请求处理
    ManimHandler.index 方法接收请求,并记录代码日志,然后调用 ManimService.executeCode 方法进行处理。

  3. 脚本预处理与执行
    在 ManimService 中:

    • 创建 cache 和 scripts 目录;
    • 替换代码中的 #(output_path) 占位符;
    • 将代码写入 script.py 文件;
    • 通过 ProcessBuilder 调用 Python 解释器执行脚本,并捕获标准输出与错误输出。
  4. 返回执行结果
    执行结果封装在 ProcessResult 对象中,其中包含:

    • 任务 ID
    • 脚本执行的标准输出和错误输出
    • 执行退出码
    • 若生成视频文件存在,则返回视频文件的路径
  5. 异常处理
    如果执行过程中发生异常,则返回 HTTP 500 状态,并在日志中记录详细的异常信息,便于调试和维护。

4.2 环境要求 ​

  • Python 环境

    • Linux/Unix 系统需安装 python3;
    • Windows 系统下安装 python。
  • 目录权限
    服务器需要具备在项目目录下创建 cache 和 scripts 子目录及写入文件的权限。


6. 总结 ​

本项目通过 Java 后端动态执行 Python (Manim) 代码,利用目录隔离、占位符替换和标准输出捕获技术,实现了一个灵活且高效的代码执行器。完整的代码实现已在本文档中展示,确保不遗漏您提供的任何代码。该方案不仅便于用户提交和执行 Manim 脚本,还通过日志记录和异常处理机制提高了系统的健壮性和可维护性。

MIT Licensed