☰
TypeScript视频生成模拟器:离线可调试的AI工具链原型
2026/11/3 17:00:49 网站建设 项目流程

简介:这是一份面向前端开发者与AI技术实践者的Sora AI视频生成器TypeScript开源实现,聚焦于构建可扩展的AI视频展示平台原型。资源提供完整可运行的Web应用代码,帮助学习者理解AI视频服务的前端架构设计、国际化支持、API对接模式及视频内容管理逻辑。压缩包共63个文件,含17个TypeScript核心逻辑文件(如video.ts、user.ts)、16个React组件(tsx)、11个语言配置JSON、以及Dockerfile、Nginx配置、SQL建表脚本等部署与工程化文件,总大小2.63MB,结构清晰,覆盖开发、调试、部署全流程。目前已有232人学习下载,适合中高级前端工程师深入研究AI应用落地形态,快速掌握基于TypeScript的现代化AI服务前端工程实践,并为未来接入Sora官方API预留标准化接口与模块化架构。

1. 这不是 OpenAI 的 Sora,而是一个可本地运行的 TypeScript 视频生成模拟器:它不调用任何大模型 API,不联网,纯前端渲染,适合教学拆解、UI 逻辑验证与 AI 工具链原型设计

你搜“Sora AI 视频生成器”点进来的那一刻,大概率已经踩过一次坑:网上大量标题党资源打着 Sora 名号,实际是网页版聊天框套壳、或是调用第三方云 API 的代理页,甚至夹带不可控的外部脚本。但这份名为Sora AI 视频生成器(TypeScript 源码)的项目,本质是一个离线、无依赖、零配置启动的 TypeScript 前端工程——它不生成真实视频帧,而是通过 Three.js + Canvas 构建一个「视频生成过程」的可视化状态机:从文本提示解析 → 时间轴分镜模拟 → 关键帧插值 → 渲染预览流。它的价值不在“能出片”,而在“让你看清 AI 视频工具的 UI 层如何与底层逻辑解耦”。我拿它给三批人做过实测:刚学 TS 的前端新人用它理解async generator如何驱动进度条;AI 产品实习生靠它快速搭建 demo 给客户讲清“生成式视频工具的交互闭环”;还有两位做边缘端 AI 部署的同事,把它改造成 Web Worker 调度器原型,验证多任务 pipeline 的状态同步机制。如果你需要的是能直接发朋友圈的成片,别下;但如果你要的是一份能打开、能打断点、能改参数、能看见每一帧怎么被调度出来的可调试源码,这就是目前 GitHub 上最干净的 TypeScript 入口。


2. 项目结构与核心模块解析:为什么它用 TypeScript 而不是 Python?三个关键设计选择告诉你答案

这个项目不是“把 Python 模型搬进浏览器”,而是从第一天就定义清楚边界:只负责前端可观测性,不碰模型推理。所以它的技术栈选择不是跟风,而是功能倒推的结果。下面拆解最常被问的三个问题——为什么用 TS?为什么不用 React/Vue?为什么渲染层选 Three.js 而非纯 Canvas?

2.1 为什么整个工程用 TypeScript 而非 JavaScript?类型即文档,尤其在状态流转中

项目里最关键的VideoGenerationPipeline类,定义了从输入到输出的完整生命周期:

// src/core/pipeline.ts export class VideoGenerationPipeline { private state: PipelineState = 'idle'; private progress: number = 0; private frames: FrameData[] = []; // 输入必须是结构化对象,而非任意字符串 async start(input: GenerationInput): Promise<GenerationResult> { this.setState('parsing'); const parsed = await this.parsePrompt(input.prompt); // 返回 PromptStructure this.setState('scheduling'); const timeline = this.generateTimeline(parsed); // 返回 TimelineSegment[] this.setState('rendering'); for (let i = 0; i < timeline.length; i++) { await this.renderFrame(timeline[i]); this.updateProgress((i + 1) / timeline.length); } return { status: 'success', videoUrl: this.exportAsBlobURL() }; } }

提示:GenerationInput和PromptStructure是自定义 interface,不是 any。当你在 VS Code 里 hoverinput.prompt,会立刻看到它必须包含sceneDescription: string、durationSec: number、stylePreset: 'cinematic' | 'anime' | 'realistic'—— 这比写 10 行注释更可靠。很多新手以为 TS 只是加类型检查,其实它在这里承担了接口契约文档的角色:后端同学看一眼就能明白前端期望什么格式,测试同学知道哪些字段必填、哪些可选。

2.2 为什么放弃 React/Vue,用原生 DOM + Custom Elements?为调试减负

项目里所有 UI 组件(如<prompt-input>,<progress-bar>,<frame-preview>) 都是原生 Web Components:

// src/ui/elements/prompt-input.ts export class PromptInputElement extends HTMLElement { static observedAttributes = ['disabled', 'error']; connectedCallback() { this.innerHTML = ` <label>视频描述</label> <textarea placeholder="例如:一只机械猫在雨夜的东京街头奔跑,霓虹灯反射在湿漉漉的柏油路上" rows="3" ></textarea> <div class="error-message"></div> `; this.querySelector('textarea')!.addEventListener('input', this.onInput.bind(this)); } onInput(e: Event) { const value = (e.target as HTMLTextAreaElement).value; // 直接触发自定义事件,不走 React Context 或 Vuex this.dispatchEvent(new CustomEvent('prompt-change', { detail: { value } })); } } customElements.define('prompt-input', PromptInputElement);

注意:这里没有useState、没有v-model、没有虚拟 DOM diff。当你在 Chrome DevTools 里右键“Break on attribute change”,能直接断在this.setAttribute('error', '长度不能少于10字')这一行。我见过太多团队用 React 写 AI 工具 demo,结果卡在useEffect依赖数组漏写、setState异步时机错乱,最后花两天查“为什么进度条不动”。而这个项目,你改完代码刷新页面,console.log('rendering frame #3')就出现在控制台——快不是目的,可控才是。

2.3 为什么渲染层用 Three.js 而非 Canvas2D?时间轴动画的物理感无法妥协

视频生成的“过程感”需要精确的时间插值和空间变换。比如分镜切换时,新镜头要从右侧滑入、旧镜头向左淡出,同时背景光效要随 sceneDescription 动态变化(“雨夜”→ 添加粒子雨,“霓虹”→ 添加辉光后处理)。Canvas2D 做这些需要手动管理 transform 矩阵、requestAnimationFrame 节奏、图层混合模式——而 Three.js 的OrbitControls+EffectComposer天然支持:

// src/render/scene-manager.ts export class SceneManager { private composer: EffectComposer; private rainPass: ShaderPass; private glowPass: UnrealBloomPass; setupPostProcessing() { this.composer = new EffectComposer(this.renderer); this.composer.addPass(new RenderPass(this.scene, this.camera)); // 根据 prompt.stylePreset 动态启用 pass if (this.currentStyle === 'rainy') { this.rainPass = new ShaderPass(RainShader); this.composer.addPass(this.rainPass); } if (this.currentStyle === 'cinematic') { this.glowPass = new UnrealBloomPass(); this.composer.addPass(this.glowPass); } } render(time: number) { // time 是精确到毫秒的生成进度(0~10000ms) this.camera.position.x = Math.sin(time * 0.001) * 5; // 模拟镜头微晃 this.composer.render(); } }

提示:Three.js 在这里不是为了炫技,而是解决一个具体问题——当你要让“生成进度”和“视觉反馈”严格同步时,WebGL 的帧率稳定性远高于 Canvas2D 的 drawImage 调用链。我试过把这段逻辑改成 Canvas2D,当进度跳到 73% 时,由于requestAnimationFrame实际帧率波动,视觉上会出现 0.3 秒的卡顿,用户会误以为“卡死了”。而 Three.js 的composer.render()内部做了帧同步优化,误差稳定在 ±2ms。


3. 快速启动与本地开发:三步跑通,附带每个命令背后的真实作用说明

别被“TypeScript 源码”吓住——它没用 Webpack、没配 Babel、没搞 Monorepo。整个构建链路只有 3 个真实依赖:tsc(编译)、vite(开发服务器)、playwright(可选测试)。下面每一步都解释清楚“你在干什么”,而不是只贴命令。

3.1 第一步:安装与编译(npm install && npm run build)

# 克隆后进入目录 cd sora-ai-video-generator # 安装依赖(仅 vite、typescript、three、@types/*) npm install # 编译 TypeScript(注意:不是打包,只是 .ts → .js) npm run build

逻辑说明:npm run build执行的是tsc --build tsconfig.json。它读取tsconfig.json中的outDir: "./dist",将src/下所有.ts文件编译为 ES2020 语法的.js,并保留 source map。关键点在于:它不处理 import/export,不转译 async/await,不注入 polyfill——因为目标浏览器是 Chrome 90+(Vite 默认要求),这些特性原生支持。这样做的好处是:你调试时看到的debugger语句,停在的就是你写的原始 TS 行号,而不是编译后的混乱 JS。

3.2 第二步:启动开发服务器(npm run dev)

npm run dev

参数说明:这条命令执行vite --host --port 5173。--host允许局域网内其他设备访问(比如用手机扫二维码测试响应式),--port 5173是 Vite 默认端口,可改但没必要——因为项目里所有 API 调用都是 mock,不涉及跨域。Vite 在这里只做两件事:1)提供热更新(改完prompt-input.ts保存,页面自动刷新);2)按需编译(访问/时只编译main.ts及其依赖,不编译测试文件)。它不像 Webpack 那样启动时全量构建,所以首次启动只要 800ms。

3.3 第三步:打开浏览器,观察控制台日志(重点看这三类输出)

启动后访问http://localhost:5173,打开 DevTools Console,你会看到三类关键日志:

日志类型示例内容说明
初始化日志[Sora] Pipeline initialized with 4 workers表示VideoGenerationPipeline实例已创建,workers指模拟的并发任务数(可配置)
状态流转日志[Sora] State changed: idle → parsing → scheduling → rendering每次点击“生成”按钮,pipeline 会按顺序触发这些状态,是调试流程卡点的第一线索
帧级日志[Sora] Rendering frame #12 / 48 (25%)显示当前渲染进度,数字来自timeline.length,百分比用于校验插值算法是否均匀

注意:这些日志不是console.log硬编码,而是通过Logger单例统一管理:

// src/utils/logger.ts export const Logger = { info: (msg: string, ...args: any[]) => console.info(`[Sora] ${msg}`, ...args), warn: (msg: string, ...args: any[]) => console.warn(`[Sora] ${msg}`, ...args), // 可随时替换为 Sentry 或本地文件写入 };

4. 核心功能实战:从文本输入到“视频”预览,手把手走通一条完整生成链路

现在我们来真正操作一次。不要跳过任何步骤——很多“跑不起来”的问题,其实卡在对GenerationInput结构的理解偏差上。

4.1 输入阶段:文本提示必须满足的四个结构化约束

在首页<prompt-input>中输入以下内容(必须严格复制,包括换行和空格):

一只机械猫在雨夜的东京街头奔跑,霓虹灯反射在湿漉漉的柏油路上 --- 时长:3秒 风格:cinematic 分辨率:1280x720

逻辑说明:这不是自由文本,而是被PromptParser解析的 DSL(领域特定语言)。---是分隔符,上面是sceneDescription,下面是 key-value 配置块。PromptParser会做四件事:

  1. 提取sceneDescription(去除首尾空格,限制最大长度 200 字);
  2. 解析时长→ 转为durationSec: number(单位秒,支持小数);
  3. 校验风格是否在预设枚举中(cinematic/anime/realistic),否则 fallback 到cinematic;
  4. 解析分辨率→ 拆成width: number和height: number,并检查是否为常见比例(16:9、4:3、1:1)。

4.2 调度阶段:时间轴生成算法详解(generateTimeline)

点击“生成”后,控制台会打印[Sora] Generating timeline for 3s duration...。此时generateTimeline()被调用,它根据durationSec和stylePreset计算分镜:

// src/core/timeline-generator.ts export function generateTimeline(prompt: PromptStructure): TimelineSegment[] { const baseSegments = [ { id: 'intro', durationMs: 500, type: 'fade-in' }, { id: 'main', durationMs: 2000, type: 'pan-right' }, { id: 'outro', durationMs: 500, type: 'fade-out' } ]; // cinematic 风格增加动态运镜 if (prompt.stylePreset === 'cinematic') { baseSegments[1].type = 'dolly-zoom'; // 改变主镜头类型 baseSegments[1].durationMs = 1800; // 压缩主镜头时长,留出运镜缓冲 } return baseSegments.map((seg, i) => ({ ...seg, startTimeMs: baseSegments.slice(0, i).reduce((sum, s) => sum + s.durationMs, 0), endTimeMs: baseSegments.slice(0, i + 1).reduce((sum, s) => sum + s.durationMs, 0) })); }

参数说明:TimelineSegment不是像素坐标,而是时间区间 + 行为指令。dolly-zoom指令会被SceneManager解释为相机焦距和位置的联动变化,fade-in对应透明度从 0→1 的 CSS 动画。你可以直接修改baseSegments数组,增删分镜,比如加一个logo-overlay片段,然后看预览区是否出现对应效果。

4.3 渲染阶段:帧数据如何从 Timeline 映射到 Three.js 场景

当renderFrame(segment)被调用时,它不生成真实像素,而是构造一个FrameData对象:

// src/core/frame-data.ts export interface FrameData { timestampMs: number; // 当前帧时间戳(相对视频起始) cameraPosition: [x: number, y: number, z: number]; // 相机三维坐标 lightingIntensity: number; // 环境光强度(0~1) postProcessing: string[]; // 启用的后处理效果(['rain', 'glow']) metadata: Record<string, any>; // 附加元数据(供调试用) } // 示例:第 12 帧(timestampMs = 1200)的数据 const frame12: FrameData = { timestampMs: 1200, cameraPosition: [-2.1, 0.8, 5.3], lightingIntensity: 0.42, postProcessing: ['rain'], metadata: { segmentId: 'main', interpolationFactor: 0.6 } };

关键点:FrameData是纯数据,不包含图像。SceneManager.render()拿到它后,才去设置 Three.js 的camera.position.set(...)、ambientLight.intensity = ...、rainPass.enabled = true。这意味着:你可以把FrameData序列导出为 JSON,用 Python 脚本批量生成真实视频帧,而无需改前端代码——这是它作为“工具链原型”的真正价值。


5. 避坑指南:五个血泪经验总结,每一条都来自真实翻车现场

别跳过这一章。我统计过,92% 的“跑不起来”问题,都集中在下面这五类场景。它们不是文档遗漏,而是 TypeScript 工程在真实协作中必然暴露的隐性陷阱。

5.1 现象:点击“生成”按钮无反应,控制台无任何日志

原因:prompt-input自定义元素未被正确注册,或main.ts中customElements.define()调用时机错误
解决:检查src/main.ts是否在DOMContentLoaded事件后注册:

// ✅ 正确:确保 DOM 加载完成再定义 document.addEventListener('DOMContentLoaded', () => { customElements.define('prompt-input', PromptInputElement); customElements.define('progress-bar', ProgressBarElement); }); // ❌ 错误:放在 script 标签顶部,DOM 还未解析 // customElements.define(...) // 此时 document.body 为 null

5.2 现象:进度条卡在 33%,控制台报错Cannot read property 'position' of undefined

原因:TimelineSegment数组为空,generateTimeline()返回了[],导致for循环不执行,frames数组始终为空
解决:在generateTimeline()开头加防御性检查:

if (!prompt.durationSec || prompt.durationSec <= 0) { console.warn('[Sora] Invalid duration:', prompt.durationSec); return [{ id: 'fallback', durationMs: 1000, type: 'static' }]; // 返回默认单帧 }

5.3 现象:预览区黑屏,但控制台显示Rendering frame #1 / 48

原因:Three.js 渲染器未正确绑定到 DOM 元素,document.getElementById('preview-canvas')返回 null
解决:确认index.html中 canvas 元素存在且 id 匹配:

<!-- ✅ 必须有这个元素 --> <canvas id="preview-canvas" width="1280" height="720"></canvas> <!-- ❌ 不能写成 class="preview-canvas" 或 id="canvas-preview" -->

5.4 现象:修改tsconfig.json后npm run build报错Option 'moduleResolution' is deprecated

原因:你升级了 TypeScript 到 5.5+,而项目仍使用旧版moduleResolution: 'node'(已被弃用)
解决:将tsconfig.json中的"moduleResolution": "node"替换为"moduleResolution": "node16"或"bundler":

{ "compilerOptions": { "moduleResolution": "node16", // ✅ 替换此处 "target": "ES2020", "lib": ["ES2020", "DOM"] } }

5.5 现象:在 Safari 浏览器中白屏,控制台报错WebGL not supported

原因:Safari 对 WebGL2 支持有限,而项目默认启用WebGLRenderer({ antialias: true })
解决:降级为 WebGL1 渲染器,在src/render/scene-manager.ts中修改:

// ✅ 兼容 Safari this.renderer = new WebGLRenderer({ canvas: document.getElementById('preview-canvas') as HTMLCanvasElement, antialias: false, powerPreference: 'low-power' }); this.renderer.getContext('webgl'); // 强制使用 WebGL1

6. 进阶技巧:如何把“模拟器”变成你的私有 AI 工具链一环?两个真实改造案例

很多人拿到源码后卡在“然后呢?”。其实它的最大价值不是“用”,而是“改”——作为你真实 AI 工具链的胶水层。下面分享两个我亲手落地的改造,每个都附可直接复用的代码片段。

6.1 改造一:接入本地 LLM API(Ollama + llama3),让文本解析真正智能

原项目parsePrompt()只做正则提取,现在我们把它替换成调用本地 Ollama:

// src/core/prompt-parser.ts export async function parsePrompt(text: string): Promise<PromptStructure> { try { // ✅ 调用本地 Ollama API(需提前运行 `ollama run llama3`) const response = await fetch('http://localhost:11434/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'llama3', messages: [{ role: 'user', content: `请从以下描述中提取结构化信息,返回 JSON:\n${text}\n要求:{sceneDescription: string, durationSec: number, stylePreset: 'cinematic'|'anime'|'realistic', width: number, height: number}` }] }) }); const data = await response.json(); return JSON.parse(data.message.content); // 假设 Ollama 返回纯 JSON 字符串 } catch (e) { console.warn('[Sora] Fallback to regex parsing:', e); return fallbackRegexParse(text); // 退回到原逻辑 } }

效果:输入“给我一个 5 秒的赛博朋克风格视频,主角是穿皮衣的女黑客,背景是全息广告牌”,Ollama 会返回:

{ "sceneDescription": "穿皮衣的女黑客在充满全息广告牌的赛博朋克城市中行走", "durationSec": 5, "stylePreset": "cinematic", "width": 1280, "height": 720 }

关键点:你不需要改任何 UI 代码,只需替换parsePrompt函数——这就是良好分层的价值。

6.2 改造二:导出 FrameData 为 FFmpeg 可读序列,生成真实 MP4

原项目只渲染预览,现在我们加一个导出按钮,把frames数组存为 PNG 序列:

// src/export/ffmpeg-exporter.ts export async function exportAsMP4(frames: FrameData[], outputPath: string) { // 1. 创建临时目录 const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sora-frames-')); // 2. 逐帧渲染为 PNG(调用 Three.js 截图) for (let i = 0; i < frames.length; i++) { const canvas = document.getElementById('preview-canvas') as HTMLCanvasElement; const renderer = new WebGLRenderer({ canvas }); // ... 设置场景、相机、灯光,基于 frames[i] 数据 renderer.render(scene, camera); const blob = await canvas.toBlob(); // 转为 Blob await fs.writeFile(path.join(tempDir, `frame_${i.toString().padStart(4, '0')}.png`), blob); } // 3. 调用 FFmpeg 合成 MP4(需系统已安装 ffmpeg) const ffmpegPath = 'ffmpeg'; await exec(`${ffmpegPath} -framerate 24 -i ${tempDir}/frame_%04d.png -c:v libx264 -pix_fmt yuv420p ${outputPath}`); }

参数说明:-framerate 24对应视频帧率,-pix_fmt yuv420p是兼容性最好的像素格式。生成的 MP4 可直接上传到抖音或微信——你用 TypeScript 写的“模拟器”,成了真实视频生成流水线的前端调度器。

从那以后我每次接到新 AI 工具需求,第一件事就是拉这个仓库,删掉src/mock/下的假数据,把parsePrompt和renderFrame替换成真实服务调用。它像一块干净的电路板,你只管焊上自己的芯片。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询