Tio Boot DocsTio Boot Docs
Home
  • java-db
  • api-table
  • Enjoy
  • Tio Boot Admin
  • ai_agent
  • translator
  • knowlege_base
  • ai-search
  • 案例
Abount
  • Github
  • Gitee
Home
  • java-db
  • api-table
  • Enjoy
  • Tio Boot Admin
  • ai_agent
  • translator
  • knowlege_base
  • ai-search
  • 案例
Abount
  • Github
  • Gitee
  • 01_tio-boot 简介

    • tio-boot:新一代高性能 Java Web 开发框架
    • tio-boot 入门示例
    • Tio-Boot 配置 : 现代化的配置方案
    • tio-boot 整合 Logback
    • tio-boot 整合 hotswap-classloader 实现热加载
    • 自行编译 tio-boot
    • 最新版本
    • 开发规范
  • 02_部署

    • 使用 Maven Profile 实现分环境打包 tio-boot 项目
    • Maven 项目配置详解:依赖与 Profiles 配置
    • tio-boot 打包成 FastJar
    • 使用 GraalVM 构建 tio-boot Native 程序
    • 使用 Docker 部署 tio-boot
    • 部署到 Fly.io
    • 部署到 AWS Lambda
    • 到阿里云云函数
    • 使用 Deploy 工具部署
    • 胖包与瘦包的打包与部署
    • 使用 Jenkins 部署 Tio-Boot 项目
    • 使用 Nginx 反向代理 Tio-Boot
    • 使用 Supervisor 管理 Java 应用
  • 03_配置

    • 配置参数
    • 服务器监听器
    • 内置缓存系统 AbsCache
    • 使用 Redis 作为内部 Cache
    • 静态文件处理器
    • 基于域名的静态资源隔离
    • DecodeExceptionHandler
  • 04_原理

    • 生命周期
    • 请求处理流程
    • 重要的类
  • 05_json

    • Json
    • 接受 JSON 和响应 JSON
    • 响应实体类
  • 06_web

    • 概述
    • 文件上传
    • 接收请求参数
    • 接收日期参数
    • 接收数组参数
    • 返回字符串
    • 返回文本数据
    • 返回网页
    • 请求和响应字节
    • 文件下载
    • 返回视频文件并支持断点续传
    • http Session
    • Cookie
    • HttpRequest
    • HttpResponse
    • Resps
    • RespBodyVo
    • /zh/06_web/19.html
    • 全局异常处理器
    • 异步
    • 动态 返回 CSS 实现
    • 返回图片
    • Transfer-Encoding: chunked 实时音频播放
    • Server-Sent Events (SSE)
    • 接口访问统计
    • 接口请求和响应数据记录
    • 自定义 Handler 转发请求
    • 使用 HttpForwardHandler 转发所有请求
    • 跨域
    • 添加 Controller
    • 常用工具类
    • HTTP Basic 认证
    • WebJars
    • JProtobuf
  • 07_validate

    • 数据紧校验规范
    • 参数校验
  • 08_websocket

    • 使用 tio-boot 搭建 WebSocket 服务
    • WebSocket 聊天室项目示例
  • 09_java-db

    • java‑db
    • 操作数据库入门示例
    • SQL 模板
    • 数据源配置与使用
    • ActiveRecord
    • Model
    • 生成器与 Model
    • Db 工具类
    • 批量操作
    • 数据库事务处理
    • Cache 缓存
    • Dialect 多数据库支持
    • 表关联操作
    • 复合主键
    • Oracle 支持
    • Enjoy SQL 模板
    • Java-DB 整合 Enjoy 模板最佳实践
    • 多数据源支持
    • 独立使用 ActiveRecord
    • 调用存储过程
    • java-db 整合 Guava 的 Striped 锁优化
    • 生成 SQL
    • 通过实体类操作数据库
    • java-db 读写分离
    • Spring Boot 整合 Java-DB
    • like 查询
    • 常用操作示例
    • Druid 监控集成指南
    • SQL 统计
  • 10_api-table

    • ApiTable 概述
    • 使用 ApiTable 连接 SQLite
    • 使用 ApiTable 连接 Mysql
    • 使用 ApiTable 连接 Postgres
    • 使用 ApiTable 连接 TDEngine
    • 使用 api-table 连接 oracle
    • 使用 api-table 连接 mysql and tdengine 多数据源
    • EasyExcel 导出
    • EasyExcel 导入
    • TQL(Table SQL)前端输入规范
    • ApiTable 实现增删改查
    • 数组类型
    • 单独使用 ApiTable
  • 11_aop

    • JFinal-aop
    • Aop 工具类
    • 配置
    • 配置
    • 独立使用 JFinal Aop
    • @AImport
    • 原理解析
  • 12_cache

    • Caffine
    • Jedis-redis
    • hutool RedisDS
    • Redisson
    • Caffeine and redis
    • CacheUtils 工具类
    • 使用 CacheUtils 整合 caffeine 和 redis 实现的两级缓存
    • 使用 java-db 整合 ehcache
    • 使用 java-db 整合 redis
    • Java DB Redis 相关 Api
    • redis 使用示例
  • 13_认证和权限

    • hutool-JWT
    • FixedTokenInterceptor
    • 使用内置 TokenManager 实现登录
    • 用户系统
    • 重置密码
    • 匿名登录
    • Google 登录
    • 权限校验注解
    • Sa-Token
    • sa-token 登录注册
    • StpUtil.isLogin() 源码解析
    • 短信登录
    • 移动端微信登录实现指南
    • 移动端重置密码
  • 14_i18n

    • i18n
  • 15_enjoy

    • tio-boot 整合 Enjoy 模版引擎文档
    • 引擎配置
    • 表达式
    • 指令
    • 注释
    • 原样输出
    • Shared Method 扩展
    • Shared Object 扩展
    • Extension Method 扩展
    • Spring boot 整合
    • 独立使用 Enjoy
    • tio-boot enjoy 自定义指令 localeDate
    • PromptEngine
    • Enjoy 入门示例-擎渲染大模型请求体
    • Enjoy 使用示例
  • 16_定时任务

    • Quartz 定时任务集成指南
    • 分布式定时任务 xxl-jb
    • cron4j 使用指南
  • 17_tests

    • TioBootTest 类
  • 18_tio

    • TioBootServer
    • tio-core
    • 内置 TCP 处理器
    • 独立启动 UDPServer
    • 使用内置 UDPServer
    • t-io 消息处理流程
    • tio-运行原理详解
    • TioConfig
    • ChannelContext
    • Tio 工具类
    • 业务数据绑定
    • 业务数据解绑
    • 发送数据
    • 关闭连接
    • Packet
    • 监控: 心跳
    • 监控: 客户端的流量数据
    • 监控: 单条 TCP 连接的流量数据
    • 监控: 端口的流量数据
    • 单条通道统计: ChannelStat
    • 所有通道统计: GroupStat
    • 资源共享
    • 成员排序
    • ssl
    • DecodeRunnable
    • 使用 AsynchronousSocketChannel 响应数据
    • 拉黑 IP
    • 深入解析 Tio 源码:构建高性能 Java 网络应用
  • 19_aio

    • ByteBuffer
    • AIO HTTP 服务器
    • 自定义和线程池和池化 ByteBuffer
    • AioHttpServer 应用示例 IP 属地查询
    • 手写 AIO Http 服务器
  • 20_netty

    • Netty TCP Server
    • Netty Web Socket Server
    • 使用 protoc 生成 Java 包文件
    • Netty WebSocket Server 二进制数据传输
    • Netty 组件详解
  • 21_netty-boot

    • Netty-Boot
    • 原理解析
    • 整合 Hot Reload
    • 整合 数据库
    • 整合 Redis
    • 整合 Elasticsearch
    • 整合 Dubbo
    • Listener
    • 文件上传
    • 拦截器
    • Spring Boot 整合 Netty-Boot
    • SSL 配置指南
    • ChannelInitializer
    • Reserve
  • 22_MQ

    • Mica-mqtt
    • EMQX
    • Disruptor
  • 23_tio-utils

    • tio-utils
    • HttpUtils
    • Notification
    • 邮箱
    • JSON
    • 读取文件
    • Base64
    • 上传和下载
    • Http
    • Telegram
    • RsaUtils
    • EnvUtils 使用文档
    • 系统监控
    • 毫秒并发 ID (MCID) 生成方案
  • 24_tio-http-server

    • 使用 Tio-Http-Server 搭建简单的 HTTP 服务
    • tio-boot 添加 HttpRequestHandler
    • 在 Android 上使用 tio-boot 运行 HTTP 服务
    • tio-http-server-native
    • handler 常用操作
  • 25_tio-websocket

    • WebSocket 服务器
    • WebSocket Client
  • 26_tio-im

    • 通讯协议文档
    • ChatPacket.proto 文档
    • java protobuf
    • 数据表设计
    • 创建工程
    • 登录
    • 历史消息
    • 发消息
  • 27_mybatis

    • Tio-Boot 整合 MyBatis
    • 使用配置类方式整合 MyBatis
    • 整合数据源
    • 使用 mybatis-plus 整合 tdengine
    • 整合 mybatis-plus
  • 28_mongodb

    • tio-boot 使用 mongo-java-driver 操作 mongodb
  • 29_elastic-search

    • Elasticsearch
    • JavaDB 整合 ElasticSearch
    • Elastic 工具类使用指南
    • Elastic-search 注意事项
    • ES 课程示例文档
  • 30_magic-script

    • tio-boot 整合 magic-script
  • 31_groovy

    • tio-boot 整合 Groovy
  • 32_firebase

    • 整合 google firebase
    • Firebase Storage
    • Firebase Authentication
    • 使用 Firebase Admin SDK 进行匿名用户管理与自定义状态标记
    • 导出用户
    • 注册回调
    • 登录注册
  • 33_文件存储

    • 文件上传数据表
    • 本地存储
    • 使用 AWS S3 存储文件并整合到 Tio-Boot 项目中
    • 存储文件到 腾讯 COS
  • 34_spider

    • jsoup
    • 爬取 z-lib.io 数据
    • 整合 WebMagic
    • WebMagic 示例:爬取学校课程数据
    • Playwright
    • Flexmark (Markdown 处理器)
    • tio-boot 整合 Playwright
    • 缓存网页数据
  • 36_integration_thirty_party

    • tio-boot 整合 okhttp
    • 整合 GrpahQL
    • 集成 Mailjet
    • 整合 ip2region
    • 整合 GeoLite 离线库
    • 整合 Lark 机器人指南
    • 集成 Lark Mail 实现邮件发送
    • Thymeleaf
    • Swagger
    • Clerk 验证
  • 37_dubbo

    • 概述
    • dubbo 2.6.0
    • dubbo 2.6.0 调用过程
    • dubbo 3.2.0
  • 38_spring

    • Spring Boot Web 整合 Tio Boot
    • spring-boot-starter-webflux 整合 tio-boot
    • Tio Boot 整合 Spring Boot Starter
    • Tio Boot 整合 Spring Boot Starter Data Redis 指南
  • 39_spring-cloud

    • tio-boot spring-cloud
  • 40_mysql

    • 使用 Docker 运行 MySQL
    • /zh/42_mysql/02.html
  • 41_postgresql

    • PostgreSQL 安装
    • PostgreSQL 主键自增
    • PostgreSQL 日期类型
    • Postgresql 金融类型
    • PostgreSQL 数组类型
    • PostgreSQL 全文检索
    • PostgreSQL 查询优化
    • 获取字段类型
    • PostgreSQL 向量
    • PostgreSQL 优化向量查询
    • PostgreSQL 其他
  • 43_oceanbase

    • 快速体验 OceanBase 社区版
    • 快速上手 OceanBase 数据库单机部署与管理
    • 诊断集群性能
    • 优化 SQL 性能指南
    • /zh/43_oceanbase/05.html
  • 50_media

    • JAVE 提取视频中的声音
    • Jave 提取视频中的图片
    • /zh/50_media/03.html
  • 51_asr

    • Whisper-JNI
  • 54_native-media

    • java-native-media
    • JNI 入门示例
    • mp3 拆分
    • mp4 转 mp3
    • 使用 libmp3lame 实现高质量 MP3 编码
    • Linux 编译
    • macOS 编译
    • 从 JAR 包中加载本地库文件
    • 支持的音频和视频格式
    • 任意格式转为 mp3
    • 通用格式转换
    • 通用格式拆分
    • 视频合并
    • VideoToHLS
    • split_video_to_hls 支持其他语言
    • 持久化 HLS 会话
  • 55_telegram4j

    • 数据库设计
    • /zh/55_telegram4j/02.html
    • 基于 MTProto 协议开发 Telegram 翻译机器人
    • 过滤旧消息
    • 保存机器人消息
    • 定时推送
    • 增加命令菜单
    • 使用 telegram-Client
    • 使用自定义 StoreLayout
    • 延迟测试
    • Reactor 错误处理
    • Telegram4J 常见错误处理指南
  • 56_telegram-bots

    • TelegramBots 入门指南
    • 使用工具库 telegram-bot-base 开发翻译机器人
  • 60_LLM

    • 简介
    • AI 问答
    • /zh/60_LLM/03.html
    • /zh/60_LLM/04.html
    • 增强检索(RAG)
    • 结构化数据检索
    • 搜索+AI
    • 集成第三方 API
    • 后置处理
    • 推荐问题生成
    • 连接代码执行器
    • 避免 GPT 混乱
    • /zh/60_LLM/13.html
  • 61_ai_agent

    • 数据库设计
    • 示例问题管理
    • 会话管理
    • 历史记录
    • 对接 Perplexity API
    • 意图识别与生成提示词
    • 智能问答模块设计与实现
    • 文件上传与解析文档
    • 翻译
    • 名人搜索功能实现
    • Ai studio gemini youbue 问答使用说明
    • 自建 YouTube 字幕问答系统
    • 自建 获取 youtube 字幕服务
    • 通用搜索
    • /zh/61_ai_agent/15.html
    • 16
    • 17
    • 18
    • 在 tio-boot 应用中整合 ai-agent
    • 16
  • 62_translator

    • 简介
  • 63_knowlege_base

    • 数据库设计
    • 用户登录实现
    • 模型管理
    • 知识库管理
    • 文档拆分
    • 片段向量
    • 命中测试
    • 文档管理
    • 片段管理
    • 问题管理
    • 应用管理
    • 向量检索
    • 推理问答
    • 问答模块
    • 统计分析
    • 用户管理
    • api 管理
    • 存储文件到 S3
    • 文档解析优化
    • 片段汇总
    • 段落分块与检索
    • 多文档解析
    • 对话日志
    • 检索性能优化
    • Milvus
    • 文档解析方案和费用对比
    • 离线运行向量模型
  • 64_ai-search

    • ai-search 项目简介
    • ai-search 数据库文档
    • ai-search SearxNG 搜索引擎
    • ai-search Jina Reader API
    • ai-search Jina Search API
    • ai-search 搜索、重排与读取内容
    • ai-search PDF 文件处理
    • ai-search 推理问答
    • Google Custom Search JSON API
    • ai-search 意图识别
    • ai-search 问题重写
    • ai-search 系统 API 接口 WebSocket 版本
    • ai-search 搜索代码实现 WebSocket 版本
    • ai-search 生成建议问
    • ai-search 生成问题标题
    • ai-search 历史记录
    • Discover API
    • 翻译
    • Tavily Search API 文档
    • 对接 Tavily Search
    • 火山引擎 DeepSeek
    • 对接 火山引擎 DeepSeek
    • ai-search 搜索代码实现 SSE 版本
    • jar 包部署
    • Docker 部署
    • 爬取一个静态网站的所有数据
    • 网页数据预处理
    • 网页数据检索与问答流程整合
  • 65_java-linux

    • Java 执行 python 代码
    • 通过大模型执行 Python 代码
    • MCP 协议
    • Cline 提示词
    • Cline 提示词-中文版本
  • 66_manim

    • 简介
    • Manim 开发环境搭建
    • 生成场景提示词
    • 生成代码
    • 完整脚本示例
    • 语音合成系统
    • Fish.audio TTS 接口说明文档与 Java 客户端封装
    • 整合 fishaudio 到 java-uni-ai-server 项目
    • 执行 Python (Manim) 代码
    • 使用 SSE 流式传输生成进度的实现文档
    • 整合全流程完整文档
    • HLS 动态推流技术文档
    • manim 分场景生成代码
    • 分场景运行代码及流式播放支持
    • 分场景业务端完整实现流程
    • Maiim布局管理器
    • 仅仅生成场景代码
    • 使用 modal 运行 manim 代码
    • Python 使用 Modal GPU 加速渲染
    • Modal 平台 GPU 环境下运行 Manim
    • Modal Manim OpenGL 安装与使用
    • 优化 GPU 加速
    • 生成视频封面流程
    • Java 调用 manim 命令 执行代码 生成封面
    • Manim 图像生成服务客户端文档
    • manim render help
    • 显示 中文公式
    • manimgl
    • EGL
    • /zh/66_manim/30.html
    • /zh/66_manim/31.html
    • 成本核算
    • /zh/66_manim/33.html
  • 70_tio-boot-admin

    • 入门指南
    • 初始化数据
    • token 存储
    • 与前端集成
    • 文件上传
    • 网络请求
    • 图片管理
    • /zh/70_tio-boot-admin/08.html
    • Word 管理
    • PDF 管理
    • 文章管理
    • 富文本编辑器
  • 71_tio-boot

    • /zh/71_tio-boot/01.html
    • Swagger 整合到 Tio-Boot 中的指南
    • HTTP/1.1 Pipelining 性能测试报告
  • 80_性能测试

    • 压力测试 - tio-http-serer
    • 压力测试 - tio-boot
    • 压力测试 - tio-boot-native
    • 压力测试 - netty-boot
    • 性能测试对比
    • TechEmpower FrameworkBenchmarks
    • 压力测试 - tio-boot 12 C 32G
  • 99_案例

    • 封装 IP 查询服务
    • tio-boot 案例 - 全局异常捕获与企业微信群通知
    • tio-boot 案例 - 文件上传和下载
    • tio-boot 案例 - 整合 ant design pro 增删改查
    • tio-boot 案例 - 流失响应
    • tio-boot 案例 - 增强检索
    • tio-boot 案例 - 整合 function call
    • tio-boot 案例 - 定时任务 监控 PostgreSQL、Redis 和 Elasticsearch
    • Tio-Boot 案例:使用 SQLite 整合到登录注册系统
    • tio-boot 案例 - 执行 shell 命令

manim 分场景生成代码

  • 1. 思路
    • 完整流程概述
  • 2. 提示词和交互流程示例
    • 2.1 用户主题
    • 2.2 系统提示词
    • 2.3 第 1 个提示
    • 2.4 第 2 个提示
    • 2.5 第 3 个提示
    • 2.6 第 4 个提示
    • 2.7 第 5 个提示
    • 2.8 第 6 个提示
    • 2.9 第 7 个提示
  • 3. 整理视频
  • 4. 合并视频
  • 总结

1. 思路

在传统的 Manim 渲染流程中,所有场景的帧生成、视频合成以及音频配音都会在全部完成后,才统一输出为最终的视频文件。这种方式虽能保证完整性,但等待时间较长,不利于实时播放或快速反馈。

为提升响应速度和用户体验,本方案采用“分段处理”的策略:每个场景单独渲染生成视频和音频,并在完成后立即输出。这样,用户无需等待全部场景渲染完成,就能开始播放第一个场景的视频内容。

本方案主要针对当前使用大语言模型一次性生成完整代码时遇到的两个核心问题:

  1. 生成和处理耗时过长
  2. 生成代码出错概率高,影响整体流程

为解决上述问题,我们引入了“分场景生成+实时处理”机制:

  • 第一个场景生成并渲染完成后,前端即可开始播放;
  • 后续场景的代码则按顺序依次生成,渲染成视频切片后追加至前方的 m3u8 播放列表中;
  • 若某个场景运行时出现错误,系统会启动交互流程,调用大模型修复出错代码,修复成功后继续处理下一个场景。

完整流程概述

  1. 调用大模型生成第 1 个场景的 Python 渲染脚本;
  2. 将代码发送至远程 Java-Linux 服务端,由后者负责自动执行渲染、生成视频,并提供支持 HLS 的流式播放接口;
  3. 若运行失败,由系统提示错误,并自动调用大模型生成修复后的代码;
  4. 每个场景生成完成后,其对应的 .m3u8 视频片段立即可被前端播放;
  5. 所有场景处理完毕后,服务端将各视频段合并生成完整的最终视频文件。

此方案大大降低了因生成错误导致的全流程中断风险,并显著提高了渲染系统的响应速度与用户交互体验。

如需,我可以继续补充交互流程图、各场景提示模板或示例代码。是否继续?


2. 提示词和交互流程示例

2.1 用户主题

例如本次场景生成的主题是:
为什么羽毛下落得比石头慢?

2.2 系统提示词

系统提示词(此处略)用于指导大模型生成代码,但在示例中不做展开,后续提示词均以“请仅输出可运行的 Python 脚本代码”作为主要要求。


2.3 第 1 个提示

提示词

下面是第 1 个场景的提示词,用来生成场景 1 的 Python 脚本代码。代码中要求包含主方法、语音合成、所需的 import 以及常量定义:

The Topic is:为什么羽毛下落得比石头慢?
The generated subtitles and narration must use the Chinese.
Please only output the runnable Python script code for scene 1, including the main method, speech synthesis, imports, and constant definitions.
Please output code without including any comments in the code.
To save processing and output time, please make the code as concise as possible while ensuring it runs correctly.
Because I execute the code by calling Python commands through Java, please ensure that the Python process does not block.

推理与输出示例

生成代码的推理耗时约 41.7 秒,生成的 Python 脚本代码如下:

# -*- coding: utf-8 -*-
import os
import numpy as np
import requests
from contextlib import contextmanager
from manim import *
import hashlib
from moviepy import AudioFileClip # Correct import
import manimpango # For font checking
from manim.utils.color.SVGNAMES import BROWN # Import BROWN for stone

# --- Font Check ---
# Specify a preferred Chinese font
DEFAULT_FONT = "Noto Sans CJK SC" # Example: Noto Sans Simplified Chinese
available_fonts = manimpango.list_fonts()
final_font = None

if DEFAULT_FONT in available_fonts:
    print(f"Font '{DEFAULT_FONT}' found.")
    final_font = DEFAULT_FONT
else:
    print(f"Warning: Font '{DEFAULT_FONT}' not found. Trying fallback fonts...")
    # Add more fallback Chinese fonts if needed
    fallback_fonts = ["PingFang SC", "Microsoft YaHei", "SimHei", "Arial Unicode MS"]
    found_fallback = False
    for font in fallback_fonts:
        if font in available_fonts:
            print(f"Switched to fallback font: '{font}'")
            final_font = font
            found_fallback = True
            break
    if not found_fallback:
        print(f"Warning: Neither '{DEFAULT_FONT}' nor fallback Chinese fonts found. Using Manim default. Chinese characters may not display correctly.")
        # final_font remains None

# --- Custom Colors ---
MY_SKY_BLUE = "#ADD8E6"   # Light blue background
MY_FEATHER_WHITE = "#F5F5F5"
MY_STONE_GRAY = "#808080"
MY_DARK_TEXT = "#1F2937"  # Dark text for contrast on light background
MY_WHITE = "#FFFFFF"     # For subtitles if needed on a darker overlay
MY_GOLD = "#B8860B"      # For potential highlights

# --- TTS Caching Setup ---
CACHE_DIR = r"#(output_path)/audio"
os.makedirs(CACHE_DIR, exist_ok=True)

class CustomVoiceoverTracker:
    """Tracks audio path and duration for TTS."""
    def __init__(self, audio_path, duration):
        self.audio_path = audio_path
        self.duration = duration

def get_cache_filename(text):
    """Generates a unique filename based on the text hash."""
    # Use text hash for filename consistency
    text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()
    return os.path.join(CACHE_DIR, f"{text_hash}.mp3")

@contextmanager
def custom_voiceover_tts(text, token="123456", base_url="https://uni-ai.fly.dev/api/manim/tts"):
    """Fetches TTS audio, caches it, and provides path and duration."""
    cache_file = get_cache_filename(text)
    audio_file = cache_file

    if os.path.exists(cache_file):
        # print(f"Using cached TTS for: {text[:30]}...")
        pass # Use cached file
    else:
        # print(f"Requesting TTS for: {text[:30]}...")
        try:
            input_text_encoded = requests.utils.quote(text)
            # Ensure the API call uses the provided text
            url = f"{base_url}?token={token}&input={input_text_encoded}"
            response = requests.get(url, stream=True, timeout=60)
            response.raise_for_status()
            with open(cache_file, "wb") as f:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk: f.write(chunk)
            audio_file = cache_file
            # print("TTS downloaded and cached.")
        except requests.exceptions.RequestException as e:
            print(f"TTS API request failed: {e}")
            tracker = CustomVoiceoverTracker(None, 0)
            yield tracker
            return
        except Exception as e:
            print(f"An error occurred during TTS processing: {e}")
            if os.path.exists(cache_file): os.remove(cache_file) # Clean up partial file
            tracker = CustomVoiceoverTracker(None, 0)
            yield tracker
            return

    # Get duration
    duration = 0
    if audio_file and os.path.exists(audio_file):
        try:
            with AudioFileClip(audio_file) as clip:
                duration = clip.duration
            # print(f"Audio duration: {duration:.2f}s")
        except Exception as e:
            print(f"Error processing audio file {audio_file}: {e}")
            audio_file = None
            duration = 0
    else:
        # print(f"TTS audio file not found or not created: {audio_file}")
        audio_file = None

    tracker = CustomVoiceoverTracker(audio_file, duration)
    try:
        yield tracker
    finally:
        pass # Keep cache

# -----------------------------
# CombinedScene: Why Feathers Fall Slower Than Stones
# -----------------------------
class CombinedScene(Scene):
    """
    Explains why feathers fall slower than stones.
    Scene 1: Introduction of the question.
    Uses Chinese narration and subtitles.
    """
    def setup(self):
        Scene.setup(self)
        # Set default font if found
        if final_font:
            Text.set_default(font=final_font)
        # Variable to hold the current scene number mobject
        self.current_scene_num_mob = None
        # Store elements needed across sections (though only one section here)
        self.section_elements = VGroup()
        # Set background color for the entire scene
        self.camera.background_color = MY_SKY_BLUE

    def update_scene_number(self, number_str, color=MY_DARK_TEXT):
        """Fades out the old scene number and fades in the new one."""
        new_scene_num = Text(number_str, font_size=24, color=color).to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(10)
        animations = [FadeIn(new_scene_num, run_time=0.5)]
        if self.current_scene_num_mob:
            # Ensure the previous mob is valid before fading out
            if self.current_scene_num_mob is not None:
                 animations.append(FadeOut(self.current_scene_num_mob, run_time=0.5))
        # Always play the animations when rendering a video
        self.play(*animations)
        self.current_scene_num_mob = new_scene_num # Update the reference

    def clear_section(self):
        """Clears elements specific to the current section."""
        # Clear updaters from section elements first
        for mob in self.section_elements:
             if mob is not None and hasattr(mob, 'get_updaters') and mob.get_updaters():
                 mob.clear_updaters()

        # Fade out section elements
        valid_elements = [elem for elem in self.section_elements if elem is not None]
        if valid_elements:
            # Use Group for simultaneous fade out
            # Always play the animation when rendering
            self.play(FadeOut(Group(*valid_elements)), run_time=0.75)

        # Clear the VGroup container
        self.section_elements = VGroup()
        self.wait(0.1)

    def construct(self):
        # --- Scene Setup ---
        # Background color is set in setup

        # --- Play Section 1 ---
        self.play_section_01()

        # --- Cleanup ---
        # Clear elements at the end of the scene
        self.clear_section()

        # Final wait before ending
        self.wait(1)
        # Fade out the last scene number
        if self.current_scene_num_mob:
            self.play(FadeOut(self.current_scene_num_mob))

    # --- Section 1: Introduction ---
    def play_section_01(self):
        """Section 1: Introduce the question and objects."""
        self.update_scene_number("01") # Update scene number

        # Title in Chinese
        title = Text("为什么羽毛下落得比石头慢?", font_size=48, color=MY_DARK_TEXT)
        title.to_edge(UP, buff=MED_LARGE_BUFF)
        self.section_elements.add(title) # Add to group for clearing

        # Feather (Simple Polygon Representation)
        feather_shape = Polygon(
            [-0.5, -1.5, 0], [-0.4, 0, 0], [-0.2, 1, 0], [0, 1.5, 0],
            [0.2, 1, 0], [0.4, 0, 0], [0.5, -1.5, 0],
            color=MY_FEATHER_WHITE, fill_opacity=1.0, stroke_width=1, stroke_color=MY_DARK_TEXT
        ).scale(0.8).rotate(-PI / 12)
        feather = VGroup(feather_shape).move_to(LEFT * 3 + UP * 0.5)
        feather_label = Text("羽毛", font_size=24, color=MY_DARK_TEXT).next_to(feather, DOWN)
        feather_group = VGroup(feather, feather_label)
        self.section_elements.add(feather_group)

        # Stone (Irregular Polygon)
        stone_shape = Polygon(
            [-0.8, -0.6, 0], [0.5, -0.8, 0], [0.9, 0.2, 0],
            [0.1, 0.9, 0], [-0.7, 0.5, 0],
            color=MY_STONE_GRAY, fill_opacity=1.0, stroke_width=0
        ).scale(1.2)
        stone = VGroup(stone_shape).move_to(RIGHT * 3 + UP * 0.5)
        stone_label = Text("石头", font_size=24, color=MY_DARK_TEXT).next_to(stone, DOWN)
        stone_group = VGroup(stone, stone_label)
        self.section_elements.add(stone_group)

        # Narration 1 (Chinese)
        voice_text_01 = "大家好!你有没有想过,为什么轻飘飘的羽毛,和沉甸甸的石头,从同样的高度落下时,羽毛总是慢很多呢?"
        with custom_voiceover_tts(voice_text_01) as tracker:
            if tracker.audio_path and tracker.duration > 0:
                self.add_sound(tracker.audio_path)
            else:
                print("Warning: Narration 1 TTS failed.")

            # Subtitle setup
            subtitle_voice = Text(
                voice_text_01,
                font_size=32,
                color=MY_DARK_TEXT, # Dark text on light blue background
                width=config.frame_width - 2, # Wrap text
                should_center=True
            ).to_edge(DOWN, buff=MED_SMALL_BUFF)

            # Animation Sequence
            self.play(FadeIn(title), run_time=1.5)
            self.play(
                AnimationGroup(
                    FadeIn(subtitle_voice),
                    FadeIn(feather_group, shift=UP*0.5),
                    FadeIn(stone_group, shift=UP*0.5),
                    lag_ratio=0.0 # Start simultaneously
                ),
                run_time=2.0
            )

            # Synchronization: Wait for audio duration, considering animation time
            anim_duration = 1.5 + 2.0 # Title fadein + group fadein
            # Calculate remaining time, ensuring it's not negative
            # Subtract subtitle fadeout time (0.5s)
            remaining_wait_time = max(0, tracker.duration - anim_duration - 0.5) if tracker.duration > 0 else 1.0

            if remaining_wait_time > 0:
                self.wait(remaining_wait_time)

            # Fade out subtitle
            self.play(FadeOut(subtitle_voice), run_time=0.5)

        # Keep objects on screen for a moment before clearing in construct()
        self.wait(1)

# --- Main execution block ---
if __name__ == "__main__":
    # Basic configuration
    config.pixel_height = 1080  # Set resolution height
    config.pixel_width = 1920   # Set resolution width
    config.frame_rate = 30      # Set frame rate
    config.output_file = "CombinedScene"  # Specify output filename
    config.disable_caching = True  # Disable caching

    # Set output directory using placeholder
    config.media_dir = r"#(output_path)" # IMPORTANT: Use the placeholder

    # Create and render the scene
    scene = CombinedScene()
    scene.render()

    print(f"Scene rendering finished. Output in: {config.media_dir}")

运行代码

运行完成后,返回的 m3u8 地址为:

/data/hls/500673260100370432/main.m3u8

2.4 第 2 个提示

提示词

下面是第 2 个提示的提示词,用于生成下一个场景的 Python 脚本代码,要求不包含前一场景的代码,但包含主方法、语音合成、imports 与常量定义,并要求与前一场景的代码接口风格、坐标系统和元素保持一致。如果无后续场景,则输出 done。

Please only output the runnable Python script code for next scene do not include the code from the previous scene but including the main method, speech synthesis, imports, and constant definitions.
Please output code without including any comments in the code.
Please ensure that the generated code for the next scene is synchronized with the previous scene's code in terms of interface style, coordinate system, and elements.
To save processing and output time, please make the code as concise as possible while ensuring it runs correctly.
Because I execute the code by calling Python commands through Java, please ensure that the Python process does not block.
If there is no next scene, output done.

推理与输出示例

推理耗时约 24.9 秒,生成的 Scene 2 代码如下:

# -*- coding: utf-8 -*-
import os
import numpy as np
import requests
from contextlib import contextmanager
from manim import *
import hashlib
from moviepy import AudioFileClip # Correct import
import manimpango # For font checking
from manim.utils.color.SVGNAMES import BROWN # Import BROWN for stone

# --- Font Check ---
# Specify a preferred Chinese font
DEFAULT_FONT = "Noto Sans CJK SC" # Example: Noto Sans Simplified Chinese
available_fonts = manimpango.list_fonts()
final_font = None

if DEFAULT_FONT in available_fonts:
    print(f"Font '{DEFAULT_FONT}' found.")
    final_font = DEFAULT_FONT
else:
    print(f"Warning: Font '{DEFAULT_FONT}' not found. Trying fallback fonts...")
    # Add more fallback Chinese fonts if needed
    fallback_fonts = ["PingFang SC", "Microsoft YaHei", "SimHei", "Arial Unicode MS"]
    found_fallback = False
    for font in fallback_fonts:
        if font in available_fonts:
            print(f"Switched to fallback font: '{font}'")
            final_font = font
            found_fallback = True
            break
    if not found_fallback:
        print(f"Warning: Neither '{DEFAULT_FONT}' nor fallback Chinese fonts found. Using Manim default. Chinese characters may not display correctly.")
        # final_font remains None

# --- Custom Colors ---
MY_SKY_BLUE = "#ADD8E6"   # Light blue background
MY_FEATHER_WHITE = "#F5F5F5"
MY_STONE_GRAY = "#808080"
MY_DARK_TEXT = "#1F2937"  # Dark text for contrast on light background
MY_WHITE = "#FFFFFF"     # For subtitles if needed on a darker overlay
MY_GOLD = "#B8860B"      # For potential highlights
MY_GRAVITY_RED = "#E53935" # Color for gravity arrows
MY_AIR_RESISTANCE_BLUE = "#03A9F4" # Light blue for air resistance arrows

# --- TTS Caching Setup ---
CACHE_DIR = r"#(output_path)/audio"
os.makedirs(CACHE_DIR, exist_ok=True)

class CustomVoiceoverTracker:
    """Tracks audio path and duration for TTS."""
    def __init__(self, audio_path, duration):
        self.audio_path = audio_path
        self.duration = duration

def get_cache_filename(text):
    """Generates a unique filename based on the text hash."""
    # Use text hash for filename consistency
    text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()
    return os.path.join(CACHE_DIR, f"{text_hash}.mp3")

@contextmanager
def custom_voiceover_tts(text, token="123456", base_url="https://uni-ai.fly.dev/api/manim/tts"):
    """Fetches TTS audio, caches it, and provides path and duration."""
    cache_file = get_cache_filename(text)
    audio_file = cache_file

    if os.path.exists(cache_file):
        # print(f"Using cached TTS for: {text[:30]}...")
        pass # Use cached file
    else:
        # print(f"Requesting TTS for: {text[:30]}...")
        try:
            input_text_encoded = requests.utils.quote(text)
            # Ensure the API call uses the provided text
            url = f"{base_url}?token={token}&input={input_text_encoded}"
            response = requests.get(url, stream=True, timeout=60)
            response.raise_for_status()
            with open(cache_file, "wb") as f:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk: f.write(chunk)
            audio_file = cache_file
            # print("TTS downloaded and cached.")
        except requests.exceptions.RequestException as e:
            print(f"TTS API request failed: {e}")
            tracker = CustomVoiceoverTracker(None, 0)
            yield tracker
            return
        except Exception as e:
            print(f"An error occurred during TTS processing: {e}")
            if os.path.exists(cache_file): os.remove(cache_file) # Clean up partial file
            tracker = CustomVoiceoverTracker(None, 0)
            yield tracker
            return

    # Get duration
    duration = 0
    if audio_file and os.path.exists(audio_file):
        try:
            with AudioFileClip(audio_file) as clip:
                duration = clip.duration
            # print(f"Audio duration: {duration:.2f}s")
        except Exception as e:
            print(f"Error processing audio file {audio_file}: {e}")
            audio_file = None
            duration = 0
    else:
        # print(f"TTS audio file not found or not created: {audio_file}")
        audio_file = None

    tracker = CustomVoiceoverTracker(audio_file, duration)
    try:
        yield tracker
    finally:
        pass # Keep cache

# -----------------------------
# CombinedScene: Why Feathers Fall Slower Than Stones
# -----------------------------
class CombinedScene(Scene):
    """
    Explains why feathers fall slower than stones.
    Scene 2: Introduce Gravity.
    Uses Chinese narration and subtitles.
    """
    def setup(self):
        Scene.setup(self)
        # Set default font if found
        if final_font:
            Text.set_default(font=final_font)
        # Variable to hold the current scene number mobject
        self.current_scene_num_mob = None
        # Store elements needed across sections
        self.section_elements = VGroup()
        # Set background color for the entire scene
        self.camera.background_color = MY_SKY_BLUE

    def update_scene_number(self, number_str, color=MY_DARK_TEXT):
        """Fades out the old scene number and fades in the new one."""
        new_scene_num = Text(number_str, font_size=24, color=color).to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(10)
        animations = [FadeIn(new_scene_num, run_time=0.5)]
        if self.current_scene_num_mob:
            if self.current_scene_num_mob is not None:
                 animations.append(FadeOut(self.current_scene_num_mob, run_time=0.5))
        self.play(*animations)
        self.current_scene_num_mob = new_scene_num

    def clear_section(self):
        """Clears elements specific to the current section."""
        for mob in self.section_elements:
             if mob is not None and hasattr(mob, 'get_updaters') and mob.get_updaters():
                 mob.clear_updaters()
        valid_elements = [elem for elem in self.section_elements if elem is not None]
        if valid_elements:
            self.play(FadeOut(Group(*valid_elements)), run_time=0.75)
        self.section_elements = VGroup()
        self.wait(0.1)

    def construct(self):
        self.play_section_02()
        self.clear_section()
        self.wait(1)
        if self.current_scene_num_mob:
            self.play(FadeOut(self.current_scene_num_mob))

    # --- Section 2: Gravity ---
    def play_section_02(self):
        """Section 2: Introduce Gravity."""
        self.update_scene_number("02")
        # Title/Explanation Text
        gravity_title = Text("首先,是重力的作用", font_size=40, color=MY_DARK_TEXT)
        gravity_title.to_edge(UP, buff=MED_LARGE_BUFF)
        gravity_expl = Text("地球会对所有物体产生向下的引力,这就是重力。", font_size=32, color=MY_DARK_TEXT)
        gravity_expl.next_to(gravity_title, DOWN, buff=MED_SMALL_BUFF)
        text_group = VGroup(gravity_title, gravity_expl)
        self.section_elements.add(text_group)
        # Feather (Positioned higher)
        feather_shape = Polygon(
            [-0.5, -1.5, 0], [-0.4, 0, 0], [-0.2, 1, 0], [0, 1.5, 0],
            [0.2, 1, 0], [0.4, 0, 0], [0.5, -1.5, 0],
            color=MY_FEATHER_WHITE, fill_opacity=1.0, stroke_width=1, stroke_color=MY_DARK_TEXT
        ).scale(0.8).rotate(-PI / 12)
        feather = VGroup(feather_shape).move_to(LEFT * 3 + UP * 1.5)
        self.section_elements.add(feather)
        # Stone (Positioned higher)
        stone_shape = Polygon(
            [-0.8, -0.6, 0], [0.5, -0.8, 0], [0.9, 0.2, 0],
            [0.1, 0.9, 0], [-0.7, 0.5, 0],
            color=MY_STONE_GRAY, fill_opacity=1.0, stroke_width=0
        ).scale(1.2)
        stone = VGroup(stone_shape).move_to(RIGHT * 3 + UP * 1.5)
        self.section_elements.add(stone)
        # Gravity Arrows
        gravity_arrow_feather = Arrow(
            start=feather.get_center() + UP * 0.2,
            end=feather.get_center() + DOWN * 1.5,
            color=MY_GRAVITY_RED,
            buff=0.1,
            stroke_width=6,
            max_tip_length_to_length_ratio=0.2
        )
        gravity_arrow_stone = Arrow(
            start=stone.get_center() + UP * 0.2,
            end=stone.get_center() + DOWN * 1.5,
            color=MY_GRAVITY_RED,
            buff=0.1,
            stroke_width=6,
            max_tip_length_to_length_ratio=0.2
        )
        gravity_elements = VGroup(gravity_arrow_feather, gravity_arrow_stone)
        self.section_elements.add(gravity_elements)
        # Narration 2 (Chinese)
        voice_text_02 = "首先,我们需要了解重力。地球吸引着它附近的所有物体,无论是羽毛还是石头,都会受到一个向下的拉力,这就是重力。"
        with custom_voiceover_tts(voice_text_02) as tracker:
            if tracker.audio_path and tracker.duration > 0:
                self.add_sound(tracker.audio_path)
            else:
                print("Warning: Narration 2 TTS failed.")
            subtitle_voice = Text(
                voice_text_02,
                font_size=32,
                color=MY_DARK_TEXT,
                width=config.frame_width - 2,
                should_center=True
            ).to_edge(DOWN, buff=MED_SMALL_BUFF)
            self.play(FadeIn(text_group), FadeIn(subtitle_voice), run_time=1.5)
            self.play(FadeIn(feather), FadeIn(stone), run_time=1.0)
            self.play(
                Create(gravity_arrow_feather),
                Create(gravity_arrow_stone),
                run_time=2.0
            )
            anim_duration = 1.5 + 1.0 + 2.0
            remaining_wait_time = max(0, tracker.duration - anim_duration - 0.5) if tracker.duration > 0 else 1.0
            if remaining_wait_time > 0:
                self.wait(remaining_wait_time)
            self.play(FadeOut(subtitle_voice), run_time=0.5)
        self.wait(1)

# --- Main execution block ---
if __name__ == "__main__":
    config.pixel_height = 1080
    config.pixel_width = 1920
    config.frame_rate = 30
    config.output_file = "CombinedScene"
    config.disable_caching = True
    config.media_dir = r"#(output_path)"
    scene = CombinedScene()
    scene.render()
    print(f"Scene rendering finished. Output in: {config.media_dir}")

运行代码

运行后返回的 m3u8 地址为:

/data/hls/500674430197940224/main.m3u8

2.5 第 3 个提示

提示词

omit

推理与输出示例

推理耗时约 30.5 秒,生成的 Scene 3 代码如下(本场景用于介绍空气阻力的初步效果):

# -*- coding: utf-8 -*-
import os
import numpy as np
import requests
from contextlib import contextmanager
from manim import *
import hashlib
from moviepy import AudioFileClip # Correct import
import manimpango # For font checking
from manim.utils.color.SVGNAMES import BROWN # Import BROWN for stone

# --- Font Check ---
# Specify a preferred Chinese font
DEFAULT_FONT = "Noto Sans CJK SC" # Example: Noto Sans Simplified Chinese
available_fonts = manimpango.list_fonts()
final_font = None

if DEFAULT_FONT in available_fonts:
    print(f"Font '{DEFAULT_FONT}' found.")
    final_font = DEFAULT_FONT
else:
    print(f"Warning: Font '{DEFAULT_FONT}' not found. Trying fallback fonts...")
    fallback_fonts = ["PingFang SC", "Microsoft YaHei", "SimHei", "Arial Unicode MS"]
    found_fallback = False
    for font in fallback_fonts:
        if font in available_fonts:
            print(f"Switched to fallback font: '{font}'")
            final_font = font
            found_fallback = True
            break
    if not found_fallback:
        print(f"Warning: Neither '{DEFAULT_FONT}' nor fallback Chinese fonts found. Using Manim default. Chinese characters may not display correctly.")

# --- Custom Colors ---
MY_SKY_BLUE = "#ADD8E6"
MY_FEATHER_WHITE = "#F5F5F5"
MY_STONE_GRAY = "#808080"
MY_DARK_TEXT = "#1F2937"
MY_WHITE = "#FFFFFF"
MY_GOLD = "#B8860B"
MY_GRAVITY_RED = "#E53935"
MY_AIR_RESISTANCE_BLUE = "#03A9F4"

# --- TTS Caching Setup ---
CACHE_DIR = r"#(output_path)/audio"
os.makedirs(CACHE_DIR, exist_ok=True)

class CustomVoiceoverTracker:
    """Tracks audio path and duration for TTS."""
    def __init__(self, audio_path, duration):
        self.audio_path = audio_path
        self.duration = duration

def get_cache_filename(text):
    """Generates a unique filename based on the text hash."""
    text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()
    return os.path.join(CACHE_DIR, f"{text_hash}.mp3")

@contextmanager
def custom_voiceover_tts(text, token="123456", base_url="https://uni-ai.fly.dev/api/manim/tts"):
    """Fetches TTS audio, caches it, and provides path and duration."""
    cache_file = get_cache_filename(text)
    audio_file = cache_file
    if os.path.exists(cache_file):
        pass
    else:
        try:
            input_text_encoded = requests.utils.quote(text)
            url = f"{base_url}?token={token}&input={input_text_encoded}"
            response = requests.get(url, stream=True, timeout=60)
            response.raise_for_status()
            with open(cache_file, "wb") as f:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk: f.write(chunk)
            audio_file = cache_file
        except requests.exceptions.RequestException as e:
            print(f"TTS API request failed: {e}")
            tracker = CustomVoiceoverTracker(None, 0)
            yield tracker
            return
        except Exception as e:
            print(f"An error occurred during TTS processing: {e}")
            if os.path.exists(cache_file): os.remove(cache_file)
            tracker = CustomVoiceoverTracker(None, 0)
            yield tracker
            return
    duration = 0
    if audio_file and os.path.exists(audio_file):
        try:
            with AudioFileClip(audio_file) as clip:
                duration = clip.duration
        except Exception as e:
            print(f"Error processing audio file {audio_file}: {e}")
            audio_file = None
            duration = 0
    else:
        audio_file = None
    tracker = CustomVoiceoverTracker(audio_file, duration)
    try:
        yield tracker
    finally:
        pass

# -----------------------------
# CombinedScene: Why Feathers Fall Slower Than Stones
# -----------------------------
class CombinedScene(Scene):
    """
    Explains why feathers fall slower than stones.
    Scene 3: Introduce Air Resistance.
    Uses Chinese narration and subtitles.
    """
    def setup(self):
        Scene.setup(self)
        if final_font:
            Text.set_default(font=final_font)
        self.current_scene_num_mob = None
        self.section_elements = VGroup()
        self.camera.background_color = MY_SKY_BLUE

    def update_scene_number(self, number_str, color=MY_DARK_TEXT):
        new_scene_num = Text(number_str, font_size=24, color=color).to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(10)
        animations = [FadeIn(new_scene_num, run_time=0.5)]
        if self.current_scene_num_mob:
            if self.current_scene_num_mob is not None:
                 animations.append(FadeOut(self.current_scene_num_mob, run_time=0.5))
        self.play(*animations)
        self.current_scene_num_mob = new_scene_num

    def clear_section(self):
        for mob in self.section_elements:
             if mob is not None and hasattr(mob, 'get_updaters') and mob.get_updaters():
                 mob.clear_updaters()
        valid_elements = [elem for elem in self.section_elements if elem is not None]
        if valid_elements:
            self.play(FadeOut(Group(*valid_elements)), run_time=0.75)
        self.section_elements = VGroup()
        self.wait(0.1)

    def construct(self):
        self.play_section_03()
        self.clear_section()
        self.wait(1)
        if self.current_scene_num_mob:
            self.play(FadeOut(self.current_scene_num_mob))

    # --- Section 3: Air Resistance ---
    def play_section_03(self):
        """Section 3: Introduce Air Resistance."""
        self.update_scene_number("03")
        # Title/Explanation Text
        air_res_title = Text("但是,还有空气阻力!", font_size=40, color=MY_DARK_TEXT)
        air_res_title.to_edge(UP, buff=MED_LARGE_BUFF)
        air_res_expl = Text("物体在空气中下落时,会受到空气向上的阻力。", font_size=32, color=MY_DARK_TEXT, width=config.frame_width - 4)
        air_res_expl.next_to(air_res_title, DOWN, buff=MED_SMALL_BUFF)
        text_group = VGroup(air_res_title, air_res_expl)
        self.section_elements.add(text_group)
        # Feather (Positioned higher)
        feather_shape = Polygon(
            [-0.5, -1.5, 0], [-0.4, 0, 0], [-0.2, 1, 0], [0, 1.5, 0],
            [0.2, 1, 0], [0.4, 0, 0], [0.5, -1.5, 0],
            color=MY_FEATHER_WHITE, fill_opacity=1.0, stroke_width=1, stroke_color=MY_DARK_TEXT
        ).scale(0.8).rotate(-PI / 12)
        feather = VGroup(feather_shape).move_to(LEFT * 3 + UP * 0.5)
        self.section_elements.add(feather)
        # Stone (Positioned higher)
        stone_shape = Polygon(
            [-0.8, -0.6, 0], [0.5, -0.8, 0], [0.9, 0.2, 0],
            [0.1, 0.9, 0], [-0.7, 0.5, 0],
            color=MY_STONE_GRAY, fill_opacity=1.0, stroke_width=0
        ).scale(1.2)
        stone = VGroup(stone_shape).move_to(RIGHT * 3 + UP * 0.5)
        self.section_elements.add(stone)
        # Gravity Arrows (Slightly faded or smaller)
        gravity_arrow_feather = Arrow(
            start=feather.get_center() + UP * 0.1,
            end=feather.get_center() + DOWN * 1.0,
            color=MY_GRAVITY_RED,
            buff=0.1, stroke_width=5, max_tip_length_to_length_ratio=0.2
        ).set_opacity(0.7)
        gravity_arrow_stone = Arrow(
            start=stone.get_center() + UP * 0.1,
            end=stone.get_center() + DOWN * 1.0,
            color=MY_GRAVITY_RED,
            buff=0.1, stroke_width=5, max_tip_length_to_length_ratio=0.2
        ).set_opacity(0.7)
        gravity_elements = VGroup(gravity_arrow_feather, gravity_arrow_stone)
        self.section_elements.add(gravity_elements)
        # Air Resistance Arrows (Upwards)
        air_res_arrow_feather = Arrow(
            start=feather.get_center() + DOWN * 0.8,
            end=feather.get_center() + UP * 1.2,
            color=MY_AIR_RESISTANCE_BLUE,
            buff=0.1,
            stroke_width=6,
            max_tip_length_to_length_ratio=0.2
        )
        air_res_arrow_stone = Arrow(
            start=stone.get_center() + DOWN * 0.8,
            end=stone.get_center() + UP * 0.4,
            color=MY_AIR_RESISTANCE_BLUE,
            buff=0.1,
            stroke_width=6,
            max_tip_length_to_length_ratio=0.2
        )
        air_res_label = Text("空气阻力 (Air Resistance)", font_size=24, color=MY_AIR_RESISTANCE_BLUE)
        air_res_label.next_to(air_res_arrow_feather, LEFT, buff=SMALL_BUFF)
        air_res_elements = VGroup(air_res_arrow_feather, air_res_arrow_stone, air_res_label)
        self.section_elements.add(air_res_elements)
        # Subtle Air Particles (Optional)
        air_particles = VGroup(*[
            Dot(point=[np.random.uniform(-config.frame_width/2+1, config.frame_width/2-1),
                       np.random.uniform(-config.frame_height/2+1, config.frame_height/2-1),
                       0],
                radius=0.02, color=MY_WHITE, fill_opacity=0.5)
            for _ in range(50)
        ]).set_z_index(-1)
        self.section_elements.add(air_particles)
        # Narration 3 (Chinese)
        voice_text_03 = "但是,除了重力,还有一个重要的力在起作用,那就是空气阻力。当物体在空气中下落时,空气会给物体一个向上的阻力。这个阻力的大小和物体的形状、表面积以及下落速度有关。"
        with custom_voiceover_tts(voice_text_03) as tracker:
            if tracker.audio_path and tracker.duration > 0:
                self.add_sound(tracker.audio_path)
            else:
                print("Warning: Narration 3 TTS failed.")
            subtitle_voice = Text(
                voice_text_03,
                font_size=32,
                color=MY_DARK_TEXT,
                width=config.frame_width - 2,
                should_center=True
            ).to_edge(DOWN, buff=MED_SMALL_BUFF)
            self.play(FadeIn(text_group), FadeIn(subtitle_voice), run_time=1.5)
            self.play(FadeIn(feather), FadeIn(stone), FadeIn(gravity_elements), FadeIn(air_particles), run_time=1.0)
            self.play(
                Create(air_res_arrow_feather),
                Create(air_res_arrow_stone),
                FadeIn(air_res_label),
                run_time=2.5
            )
            anim_duration = 1.5 + 1.0 + 2.5
            remaining_wait_time = max(0, tracker.duration - anim_duration - 0.5) if tracker.duration > 0 else 1.0
            if remaining_wait_time > 0:
                self.wait(remaining_wait_time)
            self.play(FadeOut(subtitle_voice), run_time=0.5)
        self.wait(1)

# --- Main execution block ---
if __name__ == "__main__":
    config.pixel_height = 1080
    config.pixel_width = 1920
    config.frame_rate = 30
    config.output_file = "CombinedScene"
    config.disable_caching = True
    config.media_dir = r"#(output_path)"
    scene = CombinedScene()
    scene.render()
    print(f"Scene rendering finished. Output in: {config.media_dir}")

运行代码

运行完成后返回的 m3u8 地址为:

/data/hls/500675171104964608/main.m3u8

2.6 第 4 个提示

提示词

omit

推理与输出示例

推理耗时约 32.7 秒,生成的 Scene 4 代码如下(本场景展示合力的影响和物体下落过程):

# -*- coding: utf-8 -*-
import os
import numpy as np
import requests
from contextlib import contextmanager
from manim import *
import hashlib
from moviepy import AudioFileClip # Correct import
import manimpango # For font checking
from manim.utils.color.SVGNAMES import BROWN # Import BROWN for stone

# --- Font Check ---
DEFAULT_FONT = "Noto Sans CJK SC"
available_fonts = manimpango.list_fonts()
final_font = None

if DEFAULT_FONT in available_fonts:
    print(f"Font '{DEFAULT_FONT}' found.")
    final_font = DEFAULT_FONT
else:
    print(f"Warning: Font '{DEFAULT_FONT}' not found. Trying fallback fonts...")
    fallback_fonts = ["PingFang SC", "Microsoft YaHei", "SimHei", "Arial Unicode MS"]
    found_fallback = False
    for font in fallback_fonts:
        if font in available_fonts:
            print(f"Switched to fallback font: '{font}'")
            final_font = font
            found_fallback = True
            break
    if not found_fallback:
        print(f"Warning: Neither '{DEFAULT_FONT}' nor fallback Chinese fonts found. Using Manim default. Chinese characters may not display correctly.")

# --- Custom Colors ---
MY_SKY_BLUE = "#ADD8E6"
MY_FEATHER_WHITE = "#F5F5F5"
MY_STONE_GRAY = "#808080"
MY_DARK_TEXT = "#1F2937"
MY_WHITE = "#FFFFFF"
MY_GOLD = "#B8860B"
MY_GRAVITY_RED = "#E53935"
MY_AIR_RESISTANCE_BLUE = "#03A9F4"
MY_NET_FORCE_GREEN = "#4CAF50"

# --- TTS Caching Setup ---
CACHE_DIR = r"#(output_path)/audio"
os.makedirs(CACHE_DIR, exist_ok=True)

class CustomVoiceoverTracker:
    """Tracks audio path and duration for TTS."""
    def __init__(self, audio_path, duration):
        self.audio_path = audio_path
        self.duration = duration

def get_cache_filename(text):
    """Generates a unique filename based on the text hash."""
    text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()
    return os.path.join(CACHE_DIR, f"{text_hash}.mp3")

@contextmanager
def custom_voiceover_tts(text, token="123456", base_url="https://uni-ai.fly.dev/api/manim/tts"):
    """Fetches TTS audio, caches it, and provides path and duration."""
    cache_file = get_cache_filename(text)
    audio_file = cache_file
    if os.path.exists(cache_file):
        pass
    else:
        try:
            input_text_encoded = requests.utils.quote(text)
            url = f"{base_url}?token={token}&input={input_text_encoded}"
            response = requests.get(url, stream=True, timeout=60)
            response.raise_for_status()
            with open(cache_file, "wb") as f:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk: f.write(chunk)
            audio_file = cache_file
        except requests.exceptions.RequestException as e:
            print(f"TTS API request failed: {e}")
            tracker = CustomVoiceoverTracker(None, 0)
            yield tracker
            return
        except Exception as e:
            print(f"An error occurred during TTS processing: {e}")
            if os.path.exists(cache_file): os.remove(cache_file)
            tracker = CustomVoiceoverTracker(None, 0)
            yield tracker
            return
    duration = 0
    if audio_file and os.path.exists(audio_file):
        try:
            with AudioFileClip(audio_file) as clip:
                duration = clip.duration
        except Exception as e:
            print(f"Error processing audio file {audio_file}: {e}")
            audio_file = None
            duration = 0
    else:
        audio_file = None
    tracker = CustomVoiceoverTracker(audio_file, duration)
    try:
        yield tracker
    finally:
        pass

# -----------------------------
# CombinedScene: Why Feathers Fall Slower Than Stones
# -----------------------------
class CombinedScene(Scene):
    """
    Explains why feathers fall slower than stones.
    Scene 4: Effect of Net Force.
    Uses Chinese narration and subtitles.
    """
    def setup(self):
        Scene.setup(self)
        if final_font:
            Text.set_default(font=final_font)
        self.current_scene_num_mob = None
        self.section_elements = VGroup()
        self.camera.background_color = MY_SKY_BLUE

    def update_scene_number(self, number_str, color=MY_DARK_TEXT):
        new_scene_num = Text(number_str, font_size=24, color=color).to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(10)
        animations = [FadeIn(new_scene_num, run_time=0.5)]
        if self.current_scene_num_mob:
            if self.current_scene_num_mob is not None:
                 animations.append(FadeOut(self.current_scene_num_mob, run_time=0.5))
        self.play(*animations)
        self.current_scene_num_mob = new_scene_num

    def clear_section(self):
        for mob in self.section_elements:
             if mob is not None and hasattr(mob, 'get_updaters') and mob.get_updaters():
                 mob.clear_updaters()
        valid_elements = [elem for elem in self.section_elements if elem is not None]
        if valid_elements:
            self.play(FadeOut(Group(*valid_elements)), run_time=0.75)
        self.section_elements = VGroup()
        self.wait(0.1)

    def construct(self):
        self.play_section_04()
        self.clear_section()
        self.wait(1)
        if self.current_scene_num_mob:
            self.play(FadeOut(self.current_scene_num_mob))

    # --- Section 4: Effect of Net Force ---
    def play_section_04(self):
        """Section 4: Show the effect of forces and the fall."""
        self.update_scene_number("04")
        # Title
        title = Text("合力的影响 (Effect of Net Force)", font_size=40, color=MY_DARK_TEXT)
        title.to_edge(UP, buff=MED_LARGE_BUFF)
        self.section_elements.add(title)
        # Ground Line
        ground = Line(LEFT * config.frame_width/2, RIGHT * config.frame_width/2, color=MY_GROUND_BROWN, stroke_width=5)
        ground.to_edge(DOWN, buff=1.0)
        self.section_elements.add(ground)
        # Starting Positions (Higher)
        start_y = config.frame_height / 2 - 1.5
        feather_start_pos = LEFT * 3 + UP * start_y
        stone_start_pos = RIGHT * 3 + UP * start_y
        # Feather
        feather_shape = Polygon(
            [-0.5, -1.5, 0], [-0.4, 0, 0], [-0.2, 1, 0], [0, 1.5, 0],
            [0.2, 1, 0], [0.4, 0, 0], [0.5, -1.5, 0],
            color=MY_FEATHER_WHITE, fill_opacity=1.0, stroke_width=1, stroke_color=MY_DARK_TEXT
        ).scale(0.8).rotate(-PI / 12)
        feather = VGroup(feather_shape).move_to(feather_start_pos)
        self.section_elements.add(feather)
        # Stone
        stone_shape = Polygon(
            [-0.8, -0.6, 0], [0.5, -0.8, 0], [0.9, 0.2, 0],
            [0.1, 0.9, 0], [-0.7, 0.5, 0],
            color=MY_STONE_GRAY, fill_opacity=1.0, stroke_width=0
        ).scale(1.2)
        stone = VGroup(stone_shape).move_to(stone_start_pos)
        self.section_elements.add(stone)
        # Forces Visualization (Static representation)
        gravity_feather = Arrow(feather.get_center(), feather.get_center() + DOWN * 1.0, color=MY_GRAVITY_RED, buff=0.1, stroke_width=5)
        gravity_stone = Arrow(stone.get_center(), stone.get_center() + DOWN * 1.5, color=MY_GRAVITY_RED, buff=0.1, stroke_width=5)
        air_res_feather = Arrow(feather.get_center(), feather.get_center() + UP * 0.9, color=MY_AIR_RESISTANCE_BLUE, buff=0.1, stroke_width=5)
        air_res_stone = Arrow(stone.get_center(), stone.get_center() + UP * 0.3, color=MY_AIR_RESISTANCE_BLUE, buff=0.1, stroke_width=5)
        net_force_feather = Arrow(feather.get_center()+DOWN*0.1, feather.get_center() + DOWN * 0.2, color=MY_NET_FORCE_GREEN, buff=0.1, stroke_width=6)
        net_force_stone = Arrow(stone.get_center()+DOWN*0.1, stone.get_center() + DOWN * 1.3, color=MY_NET_FORCE_GREEN, buff=0.1, stroke_width=6)
        label_gravity = Text("重力", font_size=20, color=MY_GRAVITY_RED).next_to(gravity_stone, RIGHT, buff=0.1)
        label_air_res = Text("空气阻力", font_size=20, color=MY_AIR_RESISTANCE_BLUE).next_to(air_res_stone, RIGHT, buff=0.1)
        label_net_force = Text("合力 (Net Force)", font_size=20, color=MY_NET_FORCE_GREEN).next_to(net_force_stone, RIGHT, buff=0.1)
        forces_group = VGroup(
            gravity_feather, gravity_stone, air_res_feather, air_res_stone,
            net_force_feather, net_force_stone,
            label_gravity, label_air_res, label_net_force
        )
        self.section_elements.add(forces_group)
        # Explanation Text
        explanation = Text(
            "羽毛受到的空气阻力很大,几乎抵消了重力,所以向下的合力很小。\n石头受到的空气阻力远小于其重力,所以向下的合力很大。",
            font_size=28, color=MY_DARK_TEXT, line_spacing=1.5, width=config.frame_width - 6
        ).next_to(title, DOWN, buff=MED_LARGE_BUFF)
        self.section_elements.add(explanation)
        # Narration 4 (Chinese)
        voice_text_04 = "关键就在于这两个力的合力。对于羽毛来说,它的表面积大,重量轻,所以空气阻力非常显著,几乎可以抵消掉大部分重力。这样一来,作用在羽毛上向下的合力就很小。而石头密度大,表面积相对较小,重力远大于空气阻力,因此它受到的向下的合力就大得多。根据牛顿第二定律,合力越大,加速度越大,下落得就越快。"
        with custom_voiceover_tts(voice_text_04) as tracker:
            if tracker.audio_path and tracker.duration > 0:
                self.add_sound(tracker.audio_path)
            else:
                print("Warning: Narration 4 TTS failed.")
            subtitle_voice = Text(
                voice_text_04,
                font_size=32,
                color=MY_DARK_TEXT,
                width=config.frame_width - 2,
                should_center=True
            ).to_edge(DOWN, buff=MED_SMALL_BUFF)
            self.play(FadeIn(title), FadeIn(subtitle_voice), run_time=1.0)
            self.play(FadeIn(feather), FadeIn(stone), run_time=0.5)
            self.play(FadeIn(explanation), Create(forces_group), run_time=3.0)
            self.wait(6.0)
            self.play(FadeOut(forces_group), FadeOut(explanation), run_time=0.5)
            stone_fall_anim = stone.animate.move_to(stone_start_pos + DOWN * (start_y - ground.get_y() - stone.height/2)).set_run_time(1.5).set_rate_func(rate_functions.ease_in_quad)
            feather_target_pos = feather_start_pos + DOWN * (start_y - ground.get_y() - feather.height/2) * 0.6 + RIGHT * 0.5
            feather_fall_anim = feather.animate.move_to(feather_target_pos).set_run_time(4.0).set_rate_func(rate_functions.smooth)
            self.play(stone_fall_anim, feather_fall_anim)
            anim_duration = 1.0 + 0.5 + 3.0 + 6.0 + 0.5 + 4.0
            remaining_wait_time = max(0, tracker.duration - anim_duration - 0.5) if tracker.duration > 0 else 1.0
            if remaining_wait_time > 0:
                self.wait(remaining_wait_time)
            self.play(FadeOut(subtitle_voice), run_time=0.5)
        self.wait(1)

# --- Main execution block ---
if __name__ == "__main__":
    config.pixel_height = 1080
    config.pixel_width = 1920
    config.frame_rate = 30
    config.output_file = "CombinedScene"
    config.disable_caching = True
    config.media_dir = r"#(output_path)"
    scene = CombinedScene()
    scene.render()
    print(f"Scene rendering finished. Output in: {config.media_dir}")

运行代码

运行完成返回地址:

/data/hls/500675755279237120/main.m3u8

2.7 第 5 个提示

提示词

omit

推理与输出示例

推理耗时约 30.6 秒,生成的 Scene 5 代码如下(本场景展示真空环境下物体下落的情况):

# -*- coding: utf-8 -*-
import os
import numpy as np
import requests
from contextlib import contextmanager
from manim import *
import hashlib
from moviepy import AudioFileClip # Correct import
import manimpango # For font checking
from manim.utils.color.SVGNAMES import BROWN # Import BROWN for stone

# --- Font Check ---
DEFAULT_FONT = "Noto Sans CJK SC"
available_fonts = manimpango.list_fonts()
final_font = None

if DEFAULT_FONT in available_fonts:
    print(f"Font '{DEFAULT_FONT}' found.")
    final_font = DEFAULT_FONT
else:
    print(f"Warning: Font '{DEFAULT_FONT}' not found. Trying fallback fonts...")
    fallback_fonts = ["PingFang SC", "Microsoft YaHei", "SimHei", "Arial Unicode MS"]
    found_fallback = False
    for font in fallback_fonts:
        if font in available_fonts:
            print(f"Switched to fallback font: '{font}'")
            final_font = font
            found_fallback = True
            break
    if not found_fallback:
        print(f"Warning: Neither '{DEFAULT_FONT}' nor fallback Chinese fonts found. Using Manim default. Chinese characters may not display correctly.")

# --- Custom Colors ---
MY_SKY_BLUE = "#ADD8E6"
MY_FEATHER_WHITE = "#F5F5F5"
MY_STONE_GRAY = "#808080"
MY_DARK_TEXT = "#1F2937"
MY_WHITE = "#FFFFFF"
MY_GOLD = "#B8860B"
MY_GRAVITY_RED = "#E53935"
MY_AIR_RESISTANCE_BLUE = "#03A9F4"
MY_NET_FORCE_GREEN = "#4CAF50"
MY_GROUND_BROWN = "#A1887F"
MY_VACUUM_BG = "#37474F"
MY_VACUUM_BORDER = "#B0BEC5"

# --- TTS Caching Setup ---
CACHE_DIR = r"#(output_path)/audio"
os.makedirs(CACHE_DIR, exist_ok=True)

class CustomVoiceoverTracker:
    """Tracks audio path and duration for TTS."""
    def __init__(self, audio_path, duration):
        self.audio_path = audio_path
        self.duration = duration

def get_cache_filename(text):
    """Generates a unique filename based on the text hash."""
    text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()
    return os.path.join(CACHE_DIR, f"{text_hash}.mp3")

@contextmanager
def custom_voiceover_tts(text, token="123456", base_url="https://uni-ai.fly.dev/api/manim/tts"):
    """Fetches TTS audio, caches it, and provides path and duration."""
    cache_file = get_cache_filename(text)
    audio_file = cache_file
    if os.path.exists(cache_file):
        pass
    else:
        try:
            input_text_encoded = requests.utils.quote(text)
            url = f"{base_url}?token={token}&input={input_text_encoded}"
            response = requests.get(url, stream=True, timeout=60)
            response.raise_for_status()
            with open(cache_file, "wb") as f:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk: f.write(chunk)
            audio_file = cache_file
        except requests.exceptions.RequestException as e:
            print(f"TTS API request failed: {e}")
            tracker = CustomVoiceoverTracker(None, 0)
            yield tracker
            return
        except Exception as e:
            print(f"An error occurred during TTS processing: {e}")
            if os.path.exists(cache_file): os.remove(cache_file)
            tracker = CustomVoiceoverTracker(None, 0)
            yield tracker
            return
    duration = 0
    if audio_file and os.path.exists(audio_file):
        try:
            with AudioFileClip(audio_file) as clip:
                duration = clip.duration
        except Exception as e:
            print(f"Error processing audio file {audio_file}: {e}")
            audio_file = None
            duration = 0
    else:
        audio_file = None
    tracker = CustomVoiceoverTracker(audio_file, duration)
    try:
        yield tracker
    finally:
        pass

# -----------------------------
# CombinedScene: Why Feathers Fall Slower Than Stones
# -----------------------------
class CombinedScene(Scene):
    """
    Explains why feathers fall slower than stones.
    Scene 5: In a Vacuum.
    Uses Chinese narration and subtitles.
    """
    def setup(self):
        Scene.setup(self)
        if final_font:
            Text.set_default(font=final_font)
        self.current_scene_num_mob = None
        self.section_elements = VGroup()
        # Change background to represent vacuum environment
        self.camera.background_color = MY_VACUUM_BG

    def update_scene_number(self, number_str, color=MY_WHITE):
        new_scene_num = Text(number_str, font_size=24, color=color).to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(10)
        animations = [FadeIn(new_scene_num, run_time=0.5)]
        if self.current_scene_num_mob:
            if self.current_scene_num_mob is not None:
                 animations.append(FadeOut(self.current_scene_num_mob, run_time=0.5))
        self.play(*animations)
        self.current_scene_num_mob = new_scene_num

    def clear_section(self):
        for mob in self.section_elements:
             if mob is not None and hasattr(mob, 'get_updaters') and mob.get_updaters():
                 mob.clear_updaters()
        valid_elements = [elem for elem in self.section_elements if elem is not None]
        if valid_elements:
            self.play(FadeOut(Group(*valid_elements)), run_time=0.75)
        self.section_elements = VGroup()
        self.wait(0.1)

    def construct(self):
        self.play_section_05()
        self.clear_section()
        self.wait(1)
        if self.current_scene_num_mob:
            self.play(FadeOut(self.current_scene_num_mob))

    # --- Section 5: In a Vacuum ---
    def play_section_05(self):
        """Section 5: Show the fall in a vacuum."""
        self.update_scene_number("05")
        # Title
        title = Text("在真空中会怎样? (In a Vacuum?)", font_size=40, color=MY_WHITE)
        title.to_edge(UP, buff=MED_LARGE_BUFF)
        self.section_elements.add(title)
        # Vacuum Chamber Representation
        chamber_height = config.frame_height - 3.0
        chamber_width = 6.0
        vacuum_chamber = Rectangle(
            height=chamber_height, width=chamber_width,
            color=MY_VACUUM_BORDER, stroke_width=3
        ).move_to(ORIGIN)
        chamber_label = Text("真空环境 (Vacuum)", font_size=24, color=MY_WHITE).next_to(vacuum_chamber, DOWN)
        chamber_group = VGroup(vacuum_chamber, chamber_label)
        self.section_elements.add(chamber_group)
        # Starting Positions (Inside chamber)
        start_y = vacuum_chamber.get_top()[1] - 1.0
        feather_start_pos = vacuum_chamber.get_center() + LEFT * (chamber_width/4) + UP * (chamber_height/2 - 1.0)
        stone_start_pos = vacuum_chamber.get_center() + RIGHT * (chamber_width/4) + UP * (chamber_height/2 - 1.0)
        # Feather
        feather_shape = Polygon(
            [-0.5, -1.5, 0], [-0.4, 0, 0], [-0.2, 1, 0], [0, 1.5, 0],
            [0.2, 1, 0], [0.4, 0, 0], [0.5, -1.5, 0],
            color=MY_FEATHER_WHITE, fill_opacity=1.0, stroke_width=1, stroke_color=MY_DARK_TEXT
        ).scale(0.6).rotate(-PI / 12)
        feather = VGroup(feather_shape).move_to(feather_start_pos)
        self.section_elements.add(feather)
        # Stone
        stone_shape = Polygon(
            [-0.8, -0.6, 0], [0.5, -0.8, 0], [0.9, 0.2, 0],
            [0.1, 0.9, 0], [-0.7, 0.5, 0],
            color=MY_STONE_GRAY, fill_opacity=1.0, stroke_width=0
        ).scale(0.9)
        stone = VGroup(stone_shape).move_to(stone_start_pos)
        self.section_elements.add(stone)
        # Gravity Arrows Only
        gravity_feather = Arrow(feather.get_center(), feather.get_center() + DOWN * 1.0, color=MY_GRAVITY_RED, buff=0.1, stroke_width=5)
        gravity_stone = Arrow(stone.get_center(), stone.get_center() + DOWN * 1.0, color=MY_GRAVITY_RED, buff=0.1, stroke_width=5)
        gravity_label = Text("只有重力 (Gravity Only)", font_size=20, color=MY_GRAVITY_RED).next_to(gravity_stone, RIGHT, buff=0.1)
        forces_group = VGroup(gravity_feather, gravity_stone, gravity_label)
        # Narration 5 (Chinese)
        voice_text_05 = "那么,如果在一个没有空气,也就是真空的环境里,情况会怎样呢?在真空中,没有空气阻力。唯一作用在物体上的力就是重力。根据物理学原理,在只受重力作用的情况下,所有物体,无论轻重、形状如何,都会以相同的速度下落。"
        with custom_voiceover_tts(voice_text_05) as tracker:
            if tracker.audio_path and tracker.duration > 0:
                self.add_sound(tracker.audio_path)
            else:
                print("Warning: Narration 5 TTS failed.")
            subtitle_voice = Text(
                voice_text_05,
                font_size=32,
                color=MY_WHITE,
                width=config.frame_width - 2,
                should_center=True
            ).to_edge(DOWN, buff=MED_SMALL_BUFF)
            self.play(FadeIn(title), FadeIn(chamber_group), FadeIn(subtitle_voice), run_time=1.5)
            self.play(FadeIn(feather), FadeIn(stone), run_time=1.0)
            self.play(Create(gravity_feather), Create(gravity_stone), FadeIn(gravity_label), run_time=1.5)
            target_y = vacuum_chamber.get_bottom()[1] + 0.5
            feather_target_pos = feather.get_center() * [1,0,0] + target_y * UP
            stone_target_pos = stone.get_center() * [1,0,0] + target_y * UP
            fall_duration = 2.5
            feather_fall_anim = feather.animate.move_to(feather_target_pos).set_run_time(fall_duration).set_rate_func(rate_functions.ease_in_quad)
            stone_fall_anim = stone.animate.move_to(stone_target_pos).set_run_time(fall_duration).set_rate_func(rate_functions.ease_in_quad)
            self.play(feather_fall_anim, stone_fall_anim)
            anim_duration = 1.5 + 1.0 + 1.5 + 5.0 + 0.5 + fall_duration
            remaining_wait_time = max(0, tracker.duration - anim_duration - 0.5) if tracker.duration > 0 else 1.0
            if remaining_wait_time > 0:
                self.wait(remaining_wait_time)
            self.play(FadeOut(subtitle_voice), run_time=0.5)
        self.wait(1)

# --- Main execution block ---
if __name__ == "__main__":
    config.pixel_height = 1080
    config.pixel_width = 1920
    config.frame_rate = 30
    config.output_file = "CombinedScene"
    config.disable_caching = True
    config.media_dir = r"#(output_path)"
    scene = CombinedScene()
    scene.render()
    print(f"Scene rendering finished. Output in: {config.media_dir}")

运行代码

运行结束后返回地址:

/data/hls/500677626312114176/main.m3u8

2.8 第 6 个提示

提示词

omit

推理与输出示例

推理耗时约 28.08 秒,生成的 Scene 6 代码如下(本场景为总结部分,对全部内容做概括):

# -*- coding: utf-8 -*-
import os
import numpy as np
import requests
from contextlib import contextmanager
from manim import *
import hashlib
from moviepy import AudioFileClip # Correct import
import manimpango # For font checking
from manim.utils.color.SVGNAMES import BROWN # Import BROWN for stone

# --- Font Check ---
DEFAULT_FONT = "Noto Sans CJK SC"
available_fonts = manimpango.list_fonts()
final_font = None

if DEFAULT_FONT in available_fonts:
    print(f"Font '{DEFAULT_FONT}' found.")
    final_font = DEFAULT_FONT
else:
    print(f"Warning: Font '{DEFAULT_FONT}' not found. Trying fallback fonts...")
    fallback_fonts = ["PingFang SC", "Microsoft YaHei", "SimHei", "Arial Unicode MS"]
    found_fallback = False
    for font in fallback_fonts:
        if font in available_fonts:
            print(f"Switched to fallback font: '{font}'")
            final_font = font
            found_fallback = True
            break
    if not found_fallback:
        print(f"Warning: Neither '{DEFAULT_FONT}' nor fallback Chinese fonts found. Using Manim default. Chinese characters may not display correctly.")

# --- Custom Colors ---
MY_SKY_BLUE = "#ADD8E6"
MY_FEATHER_WHITE = "#F5F5F5"
MY_STONE_GRAY = "#808080"
MY_DARK_TEXT = "#1F2937"
MY_WHITE = "#FFFFFF"
MY_GOLD = "#B8860B"
MY_GRAVITY_RED = "#E53935"
MY_AIR_RESISTANCE_BLUE = "#03A9F4"
MY_NET_FORCE_GREEN = "#4CAF50"
MY_GROUND_BROWN = "#A1887F"
MY_VACUUM_BG = "#37474F"
MY_VACUUM_BORDER = "#B0BEC5"

# --- TTS Caching Setup ---
CACHE_DIR = r"#(output_path)/audio"
os.makedirs(CACHE_DIR, exist_ok=True)

class CustomVoiceoverTracker:
    """Tracks audio path and duration for TTS."""
    def __init__(self, audio_path, duration):
        self.audio_path = audio_path
        self.duration = duration

def get_cache_filename(text):
    """Generates a unique filename based on the text hash."""
    text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()
    return os.path.join(CACHE_DIR, f"{text_hash}.mp3")

@contextmanager
def custom_voiceover_tts(text, token="123456", base_url="https://uni-ai.fly.dev/api/manim/tts"):
    """Fetches TTS audio, caches it, and provides path and duration."""
    cache_file = get_cache_filename(text)
    audio_file = cache_file
    if os.path.exists(cache_file):
        pass
    else:
        try:
            input_text_encoded = requests.utils.quote(text)
            url = f"{base_url}?token={token}&input={input_text_encoded}"
            response = requests.get(url, stream=True, timeout=60)
            response.raise_for_status()
            with open(cache_file, "wb") as f:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk: f.write(chunk)
            audio_file = cache_file
        except requests.exceptions.RequestException as e:
            print(f"TTS API request failed: {e}")
            tracker = CustomVoiceoverTracker(None, 0)
            yield tracker
            return
        except Exception as e:
            print(f"An error occurred during TTS processing: {e}")
            if os.path.exists(cache_file): os.remove(cache_file)
            tracker = CustomVoiceoverTracker(None, 0)
            yield tracker
            return
    duration = 0
    if audio_file and os.path.exists(audio_file):
        try:
            with AudioFileClip(audio_file) as clip:
                duration = clip.duration
        except Exception as e:
            print(f"Error processing audio file {audio_file}: {e}")
            audio_file = None
            duration = 0
    else:
        audio_file = None
    tracker = CustomVoiceoverTracker(audio_file, duration)
    try:
        yield tracker
    finally:
        pass

# -----------------------------
# CombinedScene: Why Feathers Fall Slower Than Stones
# -----------------------------
class CombinedScene(Scene):
    """
    Explains why feathers fall slower than stones.
    Scene 6: Conclusion/Summary.
    Uses Chinese narration and subtitles.
    """
    def setup(self):
        Scene.setup(self)
        if final_font:
            Text.set_default(font=final_font)
        self.current_scene_num_mob = None
        self.section_elements = VGroup()
        self.camera.background_color = MY_SKY_BLUE

    def update_scene_number(self, number_str, color=MY_DARK_TEXT):
        new_scene_num = Text(number_str, font_size=24, color=color).to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(10)
        animations = [FadeIn(new_scene_num, run_time=0.5)]
        if self.current_scene_num_mob:
            if self.current_scene_num_mob is not None:
                 animations.append(FadeOut(self.current_scene_num_mob, run_time=0.5))
        self.play(*animations)
        self.current_scene_num_mob = new_scene_num

    def clear_section(self):
        for mob in self.section_elements:
             if mob is not None and hasattr(mob, 'get_updaters') and mob.get_updaters():
                 mob.clear_updaters()
        valid_elements = [elem for elem in self.section_elements if elem is not None]
        if valid_elements:
            self.play(FadeOut(Group(*valid_elements)), run_time=0.75)
        self.section_elements = VGroup()
        self.wait(0.1)

    def construct(self):
        self.play_section_06()
        self.clear_section()
        self.wait(1)
        if self.current_scene_num_mob:
            self.play(FadeOut(self.current_scene_num_mob))

    # --- Section 6: Conclusion ---
    def play_section_06(self):
        """Section 6: Summarize the key points."""
        self.update_scene_number("06")
        # Title
        title = Text("总结 (Conclusion)", font_size=48, color=MY_DARK_TEXT)
        title.to_edge(UP, buff=MED_LARGE_BUFF)
        self.section_elements.add(title)
        # Summary Points
        point_size = 32
        line_width = config.frame_width - 4
        point1 = Text("• 重力对所有物体都起作用。", font_size=point_size, color=MY_DARK_TEXT, width=line_width)
        point2 = Text("• 空气阻力与物体的形状、表面积和速度有关。", font_size=point_size, color=MY_DARK_TEXT, width=line_width)
        point3 = Text("• 羽毛受到的空气阻力远大于其重力影响,石头则相反。", font_size=point_size, color=MY_DARK_TEXT, width=line_width)
        point4 = Text("• 因此,在空气中,羽毛的合力小,下落慢;石头的合力大,下落快。", font_size=point_size, color=MY_DARK_TEXT, width=line_width)
        point5 = Text("• 在真空中,没有空气阻力,两者只受重力,下落速度相同。", font_size=point_size, color=MY_DARK_TEXT, width=line_width)
        summary_group = VGroup(point1, point2, point3, point4, point5).arrange(
            DOWN, buff=MED_LARGE_BUFF * 0.8, aligned_edge=LEFT
        )
        summary_group.next_to(title, DOWN, buff=LARGE_BUFF)
        summary_group.align_on_border(LEFT, buff=1.0)
        self.section_elements.add(summary_group)
        # Narration 6 (Chinese)
        voice_text_06 = "总结一下:羽毛下落得比石头慢,主要是因为空气阻力的影响。虽然重力对两者都起作用,但羽毛轻飘飘、表面积大,受到的空气阻力非常显著,大大减缓了它的下落速度。而石头又重又密实,空气阻力对其影响小得多。所以,在空气中,石头会比羽毛下落得快得多。但在真空中,没有了空气阻力,它们就会以相同的速度下落啦!"
        with custom_voiceover_tts(voice_text_06) as tracker:
            if tracker.audio_path and tracker.duration > 0:
                self.add_sound(tracker.audio_path)
            else:
                print("Warning: Narration 6 TTS failed.")
            subtitle_voice = Text(
                voice_text_06,
                font_size=32,
                color=MY_DARK_TEXT,
                width=config.frame_width - 2,
                should_center=True
            ).to_edge(DOWN, buff=MED_SMALL_BUFF)
            self.play(FadeIn(title), FadeIn(subtitle_voice), run_time=1.0)
            self.play(FadeIn(point1, shift=UP*0.1), run_time=1.0)
            self.play(FadeIn(point2, shift=UP*0.1), run_time=1.5)
            self.play(FadeIn(point3, shift=UP*0.1), run_time=2.0)
            self.play(FadeIn(point4, shift=UP*0.1), run_time=2.0)
            self.play(FadeIn(point5, shift=UP*0.1), run_time=2.0)
            anim_duration = 1.0 + 1.0 + 1.5 + 2.0 + 2.0 + 2.0
            remaining_wait_time = max(0, tracker.duration - anim_duration - 0.5) if tracker.duration > 0 else 1.0
            if remaining_wait_time > 0:
                self.wait(remaining_wait_time)
            self.play(FadeOut(subtitle_voice), run_time=0.5)
        self.wait(1)

# --- Main execution block ---
if __name__ == "__main__":
    config.pixel_height = 1080
    config.pixel_width = 1920
    config.frame_rate = 30
    config.output_file = "CombinedScene"
    config.disable_caching = True
    config.media_dir = r"#(output_path)"
    scene = CombinedScene()
    scene.render()
    print(f"Scene rendering finished. Output in: {config.media_dir}")

运行代码

运行返回地址:

/data/hls/500678285593788416/main.m3u8

2.9 第 7 个提示

提示词

omit

推理与输出示例

推理耗时约 5 秒,输出结果为:

done

这表明大模型确认没有后续场景,生成结果为 “done”。


3. 整理视频

完成所有场景生成后,整理的视频文件 URL 如下(每个场景的视频已切片生成主 m3u8 文件,并合并成 mp4 文件):

https://manim.collegebot.ai/data/hls/500673260100370432/main.mp4
https://manim.collegebot.ai/data/hls/500674430197940224/main.mp4
https://manim.collegebot.ai/data/hls/500675171104964608/main.mp4
https://manim.collegebot.ai/data/hls/500675755279237120/main.mp4
https://manim.collegebot.ai/data/hls/500677626312114176/main.mp4
https://manim.collegebot.ai/data/hls/500678285593788416/main.mp4

对应的文件名为:

main_1.mp4
main_2.mp4
main_3.mp4
main_4.mp4
main_5.mp4
main_6.mp4

4. 合并视频

最后,通过合并各场景生成的视频切片,得到整体视频文件。下面提供了 Java 代码示例,该代码演示了如何初始化一个持久化 HLS 会话,然后追加所有视频片段,最后合并生成完整的视频。

package com.litongjava.linux.service;

import java.io.File;

import org.junit.Test;

import com.litongjava.media.NativeMedia;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class HlsSenceTest {

  @Test
  public void testSession() {

    // 配置测试用路径,请根据实际情况修改
    String playlistUrl = "./data/hls/test/main.m3u8";
    String tsPattern = "./data/hls/test/segment_video_%03d.ts";
    int startNumber = 0;
    int segmentDuration = 10; // 每个分段时长(秒)

    long sessionPtr = NativeMedia.initPersistentHls(playlistUrl, tsPattern, startNumber, segmentDuration);
    System.out.println("Session pointer: " + sessionPtr);
    String folderPath = "C:\\Users\\Administrator\\Downloads";
    File folderFile = new File(folderPath);
    File[] listFiles = folderFile.listFiles();
    for (int i = 0; i < listFiles.length; i++) {
      log.info("filename:{}", listFiles[i].getName());
      if (listFiles[i].getName().endsWith(".mp4")) {
        System.out.println(NativeMedia.appendVideoSegmentToHls(sessionPtr, listFiles[i].getAbsolutePath()));
      }
    }

    // 结束会话
    System.out.println(NativeMedia.finishPersistentHls(sessionPtr, playlistUrl));
  }
}

总结

本文档详细描述了 manim 分场景生成的解决方案,包括:

  • 背景和设计思路:通过分场景生成代码,可提高输出速度,并针对运行错误进行自动修复,确保前端能尽早播放第一个场景的视频,同时后续场景逐步追加。
  • 交互流程:通过预设提示词,依次生成每个场景的 Python 脚本代码;在生成过程中如果发生错误,则调用大模型修复代码;最后确认无后续场景时输出 “done”。
  • 完整代码示例:文档中从场景 1 到场景 7(done)的所有代码均完整展示,并附有详细的注释说明;另外,视频整理和合并视频的 Java 示例代码也完整展示,便于读者快速搭建和扩展系统。

希望本篇文档能够帮助您全面理解 manim 分场景生成的实现方案及具体代码,进而实现高效且健壮的视频自动生成系统。

Edit this page
Last Updated:
Contributors: Tong Li
Prev
HLS 动态推流技术文档
Next
分场景运行代码及流式播放支持