OpenMontage 字幕工程实战:基于 Remotion Caption JSON 的字幕生成、显示与导入全流程
字幕与旁白同步是视频生产中最高频也最容易出错的环节。OpenMontage 在 .claude/skills/remotion-best-practices/rules/subtitles.md 中为 Remotion 渲染链路制定了统一规范:所有字幕必须以 JSON 处理,统一采用 @remotion/captions 的 Caption 类型,并围绕"生成(转写)→ 显示(分页渲染)→ 导入(SRT 解析)"三个阶段给出可运行的 TypeScript 示例。读完本文,你将掌握 Caption 数据结构的字段语义、Whisper.cpp 转录到 JSON 的完整流程、TikTok 风格分页与词级高亮的实现原理,以及如何将已有 .srt 文件无缝并入 Remotion 渲染管线;同时结合 OpenMontage 仓库中的 CaptionOverlay.tsx 与 remotion_caption_burn.py 源码,理解这套规则在生产链路中的真实落地方式。
一、核心规范:所有字幕必须以 JSON 与 Caption 类型统一
规则文件开门见山,给出两条不可妥协的约束:
- 所有字幕必须用 JSON 处理,不允许在组件内直接解析文本格式或硬编码字幕串;
- 必须使用
@remotion/captions包导出的Caption类型作为字幕数据的唯一结构:
import type { Caption } from "@remotion/captions";
其完整定义如下:
type Caption = {
text: string;
startMs: number;
endMs: number;
timestampMs: number | null;
confidence: number | null;
};
各字段含义:
| 字段 | 类型 | 说明 |
|---|---|---|
text |
string |
字幕文本内容。规则特别提醒:文本对空白敏感,通常需要在每个词前保留空格(见"显示"阶段的 whiteSpace: "pre" 处理) |
startMs |
number |
该条字幕的起始时间,单位为毫秒,用于换算 Remotion 帧号 |
endMs |
number |
该条字幕的结束时间,单位为毫秒 |
timestampMs |
number | null |
可选的精确时间戳(词级时间戳场景),无值时为 null |
confidence |
number | null |
语音识别置信度,由转写引擎(如 Whisper)产生,无值时为 null |
这一设计的核心价值在于数据与表现分离:字幕只是纯数据(JSON),如何展示完全由渲染层决定。OpenMontage 的 Remotion 工程 remotion-composer/package.json 已将 @remotion/captions 固定为 ^4.0.484 依赖,与 remotion 本体版本对齐,确保类型与运行时一致。
值得说明的是,OpenMontage 在仓库内部还定义了一个贴近生产使用的变体——CaptionOverlay.tsx 中的 WordCaption 接口:
export interface WordCaption {
word: string;
startMs: number;
endMs: number;
// Force a page break after this word (e.g. sentence or scene boundaries).
// Useful for CJK captions where pages should align with clause boundaries.
pageBreakAfter?: boolean;
}
它把 Caption 的通用结构收敛为词级粒度的渲染输入,并额外提供 pageBreakAfter 标记位,用于在句末或场景边界强制分页——这对中文等无词间空格的 CJK 字幕尤其重要(规则文件本身面向空格分隔语言,CJK 场景由该组件补齐)。从源码结构看,WordCaption 是 Caption 在"词级渲染"这一具体用途上的适配,两者字段语义(startMs/endMs)完全对齐。
规则文件将该流程拆成三个子主题,并分别指向三个子规则文档:生成(transcribe-captions.md)、显示(display-captions.md)、导入(import-srt-captions.md)。下面逐一展开。
二、生成字幕:用 Whisper.cpp 把音频转写成 Caption JSON
字幕数据从哪里来?规则给出的首选方案是本地 Whisper.cpp 转写,对应 transcribe-captions.md。
2.1 安装依赖
@remotion/install-whisper-cpp 包负责下载 Whisper.cpp 可执行文件与模型,并通过 Node API 驱动转写。使用 Remotion 官方脚手架命令安装:
npx remotion add @remotion/install-whisper-cpp
2.2 转写脚本全流程
规则提供了一个可直接运行的 Node.js 脚本骨架,它完成"安装引擎 → 下载模型 → 转写 → 后处理 → 输出 JSON"五步:
import path from "path";
import {
downloadWhisperModel,
installWhisperCpp,
transcribe,
toCaptions,
} from "@remotion/install-whisper-cpp";
import fs from "fs";
const to = path.join(process.cwd(), "whisper.cpp");
await installWhisperCpp({
to,
version: "1.5.5",
});
await downloadWhisperModel({
model: "medium.en",
folder: to,
});
// Convert the audio to a 16KHz wav file first if needed:
// import {execSync} from 'child_process';
// execSync('ffmpeg -i /path/to/audio.mp4 -ar 16000 /path/to/audio.wav -y');
const whisperCppOutput = await transcribe({
model: "medium.en",
whisperPath: to,
whisperCppVersion: "1.5.5",
inputPath: "/path/to/audio123.wav",
tokenLevelTimestamps: true,
});
// Optional: Apply our recommended postprocessing
const { captions } = toCaptions({
whisperCppOutput,
});
// Write it to the public/ folder so it can be fetched from Remotion
fs.writeFileSync("captions123.json", JSON.stringify(captions, null, 2));
关键参数说明:
installWhisperCpp({ to, version }):to是 Whisper.cpp 的安装目录(示例中为当前工作目录下的whisper.cpp/),version指定引擎版本,示例固定为1.5.5,保证可复现。downloadWhisperModel({ model, folder }):模型名示例为medium.en(英文专用中量级模型,在精度与速度间平衡较好);模型文件下载到folder即to目录。如需其他语言或更小模型,可替换为tiny/base/small/large-v3等 Whisper 标准模型名。transcribe({ model, whisperPath, whisperCppVersion, inputPath, tokenLevelTimestamps }):核心转写调用。inputPath必须是音频文件路径;tokenLevelTimestamps: true开启词级(token 级)时间戳,这是后面做词高亮(karaoke 效果)的数据前提。- 输入格式约束:Whisper 对 16kHz 单声道 WAV 兼容性最佳。规则给出注释示例,先用 FFmpeg 统一转码:
ffmpeg -i /path/to/audio.mp4 -ar 16000 /path/to/audio.wav -y
toCaptions({ whisperCppOutput }):把 Whisper.cpp 的原始输出后处理为Caption[](即第一节的 JSON 结构),规则称之为"推荐的后处理",负责把引擎输出规整为符合Caption字段语义的数组。- 输出位置:写入
public/目录(示例文件captions123.json),这样 Remotion 组件才能通过staticFile()在浏览器与渲染进程中取到。
规则最后强调一条工程惯例:每个片段(clip)单独转写、单独生成 JSON 文件,而不是把整条长视频一次转写。这便于按镜头粒度管理字幕、单独重转写失败的片段,也让每份 JSON 与对应视频资源一一对应。
2.3 仓库中的配套实现
OpenMontage 将"转写得到词级时间戳"这一前置环节交由 tools/analysis/transcriber.py(transcriber 工具)完成,其产物是带 words 数组(每项含 word/start/end)的分段结构。真正消费这些词级数据并落到 Remotion 渲染的是 remotion_caption_burn.py 中的 RemotionCaptionBurn 工具:其 _segments_to_word_captions() 方法(L182-L217)把 transcriber 的词级片段转换为 [{word, startMs, endMs}, ...],秒级时间戳乘 1000 转为毫秒;当片段缺少词级时间戳时,则退化为"按词均分片段时长"的兜底策略(_segments_to_word_captions 中 per_word = dur / max(len(text_words), 1)),并支持 corrections 字典对语音识别常见误词做大小写不敏感的纠错替换——这与规则中 toCaptions 后处理的定位一致,都是"引擎输出 → 规整 Caption 数据"的适配层。
三、显示字幕:TikTok 风格分页与词级高亮
字幕数据就绪后,如何渲染?对应 display-captions.md。这套方案专为短视频竖屏场景设计:按"页"分组词、每页一个 <Sequence>、当前词高亮。
3.1 安装依赖
npx remotion add @remotion/captions
3.2 加载字幕 JSON(useDelayRender 门控渲染)
字幕是异步 fetch 的,在数据到达前不能让 Remotion 进入渲染。规则使用 useDelayRender() 挂起渲染,直到 JSON 加载完成:
import { useState, useEffect, useCallback } from "react";
import { AbsoluteFill, staticFile, useDelayRender } from "remotion";
import type { Caption } from "@remotion/captions";
export const MyComponent: React.FC = () => {
const [captions, setCaptions] = useState<Caption[] | null>(null);
const { delayRender, continueRender, cancelRender } = useDelayRender();
const [handle] = useState(() => delayRender());
const fetchCaptions = useCallback(async () => {
try {
// Assuming captions.json is in the public/ folder.
const response = await fetch(staticFile("captions123.json"));
const data = await response.json();
setCaptions(data);
continueRender(handle);
} catch (e) {
cancelRender(e);
}
}, [continueRender, cancelRender, handle]);
useEffect(() => {
fetchCaptions();
}, [fetchCaptions]);
if (!captions) {
return null;
}
return <AbsoluteFill>{/* Render captions here */}</AbsoluteFill>;
};
这里有三点设计值得注意:
delayRender()返回的handle必须保存,加载成功后调用continueRender(handle)放行渲染,失败时cancelRender(e)让渲染报错退出——避免"字幕未加载完就出帧"或"静默黑屏"两类事故;- 用
staticFile("captions123.json")引用public/下的文件,浏览器端与 headless 渲染端路径一致; - 数据未就绪时直接返回
null,不渲染任何内容。
3.3 创建分页:createTikTokStyleCaptions
规则要求用 createTikTokStyleCaptions() 把连续的词分组为"页",combineTokensWithinMilliseconds 决定每页容纳多少词:
import { useMemo } from "react";
import { createTikTokStyleCaptions } from "@remotion/captions";
import type { Caption } from "@remotion/captions";
// How often captions should switch (in milliseconds)
// Higher values = more words per page
// Lower values = fewer words (more word-by-word)
const SWITCH_CAPTIONS_EVERY_MS = 1200;
const { pages } = useMemo(() => {
return createTikTokStyleCaptions({
captions,
combineTokensWithinMilliseconds: SWITCH_CAPTIONS_EVERY_MS,
});
}, [captions]);
参数语义:数值越大,同一页内合并的词越多(接近整句一屏);数值越小,每页词越少(趋向逐词跳动)。示例取 1200ms,即大约每 1.2 秒切换一页。pages 数组由 useMemo 缓存,避免每次渲染重复分组。
3.4 用 Sequence 按页渲染
分页结果中的每一页都是一个带时间窗(startMs 及下一页的起始)的渲染单元,映射为 Remotion 的 <Sequence>,起止帧由毫秒按 fps 换算:
import { Sequence, useVideoConfig, AbsoluteFill } from "remotion";
import type { TikTokPage } from "@remotion/captions";
const CaptionedContent: React.FC = () => {
const { fps } = useVideoConfig();
return (
<AbsoluteFill>
{pages.map((page, index) => {
const nextPage = pages[index + 1] ?? null;
const startFrame = (page.startMs / 1000) * fps;
const endFrame = Math.min(
nextPage ? (nextPage.startMs / 1000) * fps : Infinity,
startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) * fps,
);
const durationInFrames = endFrame - startFrame;
if (durationInFrames <= 0) {
return null;
}
return (
<Sequence
key={index}
from={startFrame}
durationInFrames={durationInFrames}
>
<CaptionPage page={page} />
</Sequence>
);
})}
</AbsoluteFill>
);
};
帧换算公式是这套方案的技术核心:startFrame = (page.startMs / 1000) * fps。每页持续帧数取"下一页开始"与"本页最长停留时间"两者的较小值,并用 Math.min 钳制;durationInFrames <= 0 的页直接跳过(防止非法空 Sequence)。
3.5 空白保留:whiteSpace: "pre"
规则明确强调:字幕文本是空白敏感的,text 字段中每个词前应包含空格,渲染时必须用 whiteSpace: "pre" 保留这些空白,否则连续单词会粘连在一起难以阅读。这一点同样体现在 OpenMontage 的组件实现中:CaptionOverlay.tsx 的渲染容器使用 whiteSpace: "pre-wrap"(L106),并在每个词外包裹 display: "inline-block" + whiteSpace: "nowrap"(L119-L120),保证换行只发生在词边界、单词不被截断。
3.6 独立组件拆分
规则要求把字幕逻辑放进独立组件、独立文件,不要与视频主组件混写。理由很实际:字幕组件需要同时消费 useCurrentFrame()(逐帧计算当前高亮词)与 useVideoConfig()(获取 fps),与主场景耦合会显著降低可维护性。OpenMontage 的 CaptionOverlay.tsx 正是这一要求的落地产物——它被 Explainer.tsx、TalkingHead.tsx、CinematicRenderer.tsx 等多个组合复用。
3.7 词级高亮(karaoke 效果)
每一页包含 tokens(词级时间戳数组),通过对比"当前绝对时间"与"词的 fromMs/toMs 窗口"来判断当前读到的词:
import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";
import type { TikTokPage } from "@remotion/captions";
const HIGHLIGHT_COLOR = "#39E508";
const CaptionPage: React.FC<{ page: TikTokPage }> = ({ page }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Current time relative to the start of the sequence
const currentTimeMs = (frame / fps) * 1000;
// Convert to absolute time by adding the page start
const absoluteTimeMs = page.startMs + currentTimeMs;
return (
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
<div style={{ fontSize: 80, fontWeight: "bold", whiteSpace: "pre" }}>
{page.tokens.map((token) => {
const isActive =
token.fromMs <= absoluteTimeMs && token.toMs > absoluteTimeMs;
return (
<span
key={token.fromMs}
style={{ color: isActive ? HIGHLIGHT_COLOR : "white" }}
>
{token.text}
</span>
);
})}
</div>
</AbsoluteFill>
);
};
高亮判定是一个左闭右开区间:token.fromMs <= absoluteTimeMs && token.toMs > absoluteTimeMs。注意时间换算:useCurrentFrame() 给出的是当前 Sequence 内部的相对帧,必须加上 page.startMs 得到绝对时间,才能与词级时间戳对齐。规则示例用 #39E508 作为高亮色(霓虹绿),实际项目中可替换为与主题色一致的强调色。
3.8 与视频内容同屏显示
字幕默认应叠放在视频内容之上,保证音画字三者同步;并且每段视频对应一份独立的字幕 JSON:
<AbsoluteFill>
<Video src={staticFile("video.mp4")} />
<CaptionPage page={page} />
</AbsoluteFill>
3.9 OpenMontage 的增强实现
规则文件给出的是 API 层面的最小示例;OpenMontage 在 CaptionOverlay.tsx 中把同一套思想做成了可配置的生产组件,几处增强值得对照阅读:
- 分页逻辑:
buildPages()(L40-L58)以wordsPerPage(默认 6)为硬分页阈值,同时尊重pageBreakAfter强制分页标记,与规则的combineTokensWithinMilliseconds策略互为补充; - 入场动画:
PageRenderer用spring()(damping: 18, stiffness: 120)配合interpolate实现淡入 + 上移 20px 的入场(L75-L92),这符合 Remotion"动画由帧驱动、禁用 CSS transition"的规范(代码中显式注释transition: "none"); - 三态词色:当前词用
highlightColor(默认#22D3EE),已读词用原色,未读词用 60% 透明度的原色(L121),并给高亮词叠加辉光textShadow,提升复杂背景下的可读性; - CJK 适配:
wordSeparator可传""(默认" "),配合pageBreakAfter让中文字幕按小句分页,弥补规则方案对无空格语言的空缺。
四、导入字幕:从 .srt 到 Caption 的无缝转换
第三条路径是导入:如果你已有现成的 .srt 字幕文件(例如从剪辑软件或 subtitle_gen 工具导出),不必重新转写,直接用 parseSrt() 解析即可,对应 import-srt-captions.md。
4.1 安装依赖
@remotion/captions 同时承担解析与渲染职责。不同包管理器对应不同安装命令:
npx remotion add @remotion/captions # If project uses npm
bunx remotion add @remotion/captions # If project uses bun
yarn remotion add @remotion/captions # If project uses yarn
pnpm exec remotion add @remotion/captions # If project uses pnpm
4.2 读取并解析 .srt
与 3.2 相同的异步加载骨架,只是把 response.json() 换成 response.text() + parseSrt():
import { useState, useEffect, useCallback } from "react";
import { AbsoluteFill, staticFile, useDelayRender } from "remotion";
import { parseSrt } from "@remotion/captions";
import type { Caption } from "@remotion/captions";
export const MyComponent: React.FC = () => {
const [captions, setCaptions] = useState<Caption[] | null>(null);
const { delayRender, continueRender, cancelRender } = useDelayRender();
const [handle] = useState(() => delayRender());
const fetchCaptions = useCallback(async () => {
try {
const response = await fetch(staticFile("subtitles.srt"));
const text = await response.text();
const { captions: parsed } = parseSrt({ input: text });
setCaptions(parsed);
continueRender(handle);
} catch (e) {
cancelRender(e);
}
}, [continueRender, cancelRender, handle]);
useEffect(() => {
fetchCaptions();
}, [fetchCaptions]);
if (!captions) {
return null;
}
return <AbsoluteFill>{/* Use captions here */}</AbsoluteFill>;
};
parseSrt({ input: text }) 返回 { captions },其中每条字幕已规整为第一节的 Caption 结构(text/startMs/endMs)。规则还明确:远程 URL 同样受支持——只需把 staticFile("subtitles.srt") 换成 fetch("https://.../subtitles.srt") 这类远程地址,其余逻辑不变。解析完成后,字幕即可直接喂给 3.3 的 createTikTokStyleCaptions() 等所有 @remotion/captions 工具,实现"SRT 导入 → 分页渲染 → 词高亮"的完整链路。
4.3 仓库印证:SRT 在 OpenMontage 全链路中的角色
OpenMontage 中 SRT 不只是 Remotion 的输入,也作为 Remotion 不可用时的兜底媒介:
- 转写工具的输出侧:技能文档 subtitle-sync.md 规定
subtitle_gen工具可输出 SRT、VTT、Caption JSON 三种格式,其中 SRT 定位为"通用格式,兼容 FFmpeg、播放器与 YouTube 上传",Caption JSON 则专供"自定义渲染器做词级数据渲染"——与 Remotion 规则完全同构。 - SRT 的解析侧:
RemotionCaptionBurn的_srt_to_word_captions()(L219-L261)用正则解析 SRT 时间轴(HH:MM:SS,mmm --> HH:MM:SS,mmm),把整条 cue 的时长按词数均分,产出词级startMs/endMs,等效于规则中"导入后即可使用"的衔接点;工具还内置了_ms_to_srt()(L431-L437)反向把词级时间戳拼回 SRT,用于 FFmpeg 兜底烧录。 - 时间戳正确性的测试保障:测试 test_subtitle_timestamps.py 专门覆盖了 SRT/VTT 时间戳的毫秒进位问题——例如
0.9999s必须输出00:00:01,000而非非法的00:00:00,1000,并验证进位能跨分、跨时正确传播。这类边界若不处理,生成的 SRT 会被 FFmpeg 的subtitles滤镜、VLC 等严格解析器拒绝,直接破坏"SRT 导入 → Remotion 渲染"与"SRT → FFmpeg 兜底"两条链路。
五、规则在生产管线中的落地:remotion_caption_burn
把三份规则串起来看,OpenMontage 把它们固化成了 tools/video/remotion_caption_burn.py 中的 RemotionCaptionBurn 工具(name = "remotion_caption_burn",capability = "subtitle",provider = "remotion"),其执行逻辑 execute()(L443-L489)完整对应"生成 → 显示"两条规则:
- 输入二选一:
segments(transcriber 的词级片段)或srt_path,两者同时提供时segments优先; - 词级数据统一转换为
WordCaptionJSON(规则第一节的 Caption 结构在词级渲染场景的变体); - 若检测到 Remotion 环境可用(
npx存在且找到remotion-composer/下的node_modules),把视频复制到 Remotion 的public/talking-head/,连同字幕数据写入public/demo-props/下的 props JSON,然后执行:
npx remotion render TalkingHead --props=... --fps=30 --codec=h264 --crf=18
渲染由 TalkingHead.tsx 组合承接,内部正是通过 CaptionOverlay 完成规则第三节的"Sequence 分页 + 词高亮"渲染(视频时长与分辨率由 ffprobe 动态探测后传入,保证烧录后画面尺寸不变)。
- 若 Remotion 不可用,则降级为 FFmpeg
subtitles滤镜烧录(L362-L429),按每页约 4 词把词级时间戳聚合成 SRT cue,配合force_style参数(白字、粗体、黑描边、底部居中MarginV=100)保证可读性——这与 subtitle-sync.md 中关于 ASSforce_style的规范(如必须使用 8 位&H00FFFFFF颜色格式、竖屏font_size不超过 18 等)相互印证。
该工具的关键输入参数及默认值,可以看作规则在真实 API 中的参数化:
| 参数 | 默认值 | 说明 |
|---|---|---|
input_path / output_path |
必填 | 输入视频与输出路径 |
segments |
无 | transcriber 词级片段,优先于 srt_path |
srt_path |
无 | SRT 备选输入 |
words_per_page |
4 |
每页词数(对应 wordsPerPage) |
font_size |
52 |
字幕字号(fontSize) |
highlight_color |
#22D3EE |
当前词高亮色(highlightColor) |
corrections |
无 | 识别误词纠错字典,如 {"cloud": "Claude"} |
force_ffmpeg |
false |
强制走 FFmpeg 兜底 |
工具的 user_visible_verification 字段还给定了验收标准:字幕出现在画面底部、当前词按指定颜色高亮、字幕不遮挡人脸——这与规则"字幕叠放在视频内容之上、保持同步"的设计目标一一对应。
六、字幕工程的注意事项与最佳实践小结
综合规则文档与仓库实现,OpenMontage 的字幕规范可以浓缩为以下实践要点:
- 统一数据契约:一切字幕先落成 JSON,结构以
@remotion/captions的Caption为准;text/startMs/endMs是必填主干,timestampMs/confidence由转写引擎填充。生产词级渲染时可用WordCaption这类变体,但字段语义保持一致。 - 生成链路:Whisper.cpp +
tokenLevelTimestamps: true获得词级时间戳 →toCaptions()后处理 → 每片段独立 JSON 进public/。没有词级时间戳时按词均分时长兜底,但要意识到精度下降。 - 显示链路:
useDelayRender门控异步加载 →createTikTokStyleCaptions按时间窗分页 → 每页一个<Sequence>(帧号 = 毫秒 / 1000 × fps)→whiteSpace: "pre"保留空白 → 词级高亮用左闭右开区间判定;字幕逻辑必须独立成组件。 - 导入链路:
parseSrt({ input })一键把.srt转为Caption[],之后与生成链路完全同构;注意毫秒进位这类时间戳边界问题(有专门回归测试兜底)。 - 格式与可读性:转写产生的是词级 JSON,烧录输出用 SRT 更通用;字幕应固定在画面底部区域(
MarginV不超过 100,竖屏字号受限),当前词高亮色需与主题保持对比度——OpenMontage 的主题对比度契约由测试 test_theme_text_contrast_contract.py 约束,保证任何主题下字幕文字都清晰可读。
以上五条,就是 OpenMontage 中 Remotion 字幕从"一段音频"到"带词高亮的成品视频"的完整规范路径;你可以在仓库中继续阅读 subtitles.md 及其三个子规则文档获取原始规则,对照 CaptionOverlay.tsx、remotion_caption_burn.py 与 subtitle-sync.md 查看生产级实现细节。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.21 K637- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python290
cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端TypeScript2 K146
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python46267
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go20043
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java33951