byteplan-cli 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/package.json +7 -3
  2. package/skills/byteplan-analysis/SKILL.md +1078 -0
  3. package/skills/byteplan-api/API_REFERENCE.md +249 -0
  4. package/skills/byteplan-api/SKILL.md +96 -0
  5. package/skills/byteplan-api/package.json +16 -0
  6. package/skills/byteplan-api/scripts/api.js +973 -0
  7. package/skills/byteplan-excel/SKILL.md +212 -0
  8. package/skills/byteplan-excel/examples/margin-analysis.json +40 -0
  9. package/skills/byteplan-excel/package.json +12 -0
  10. package/skills/byteplan-excel/pnpm-lock.yaml +68 -0
  11. package/skills/byteplan-excel/scripts/generate_excel.js +156 -0
  12. package/skills/byteplan-html/SKILL.md +490 -0
  13. package/skills/byteplan-html/examples/example-output.html +184 -0
  14. package/skills/byteplan-html/examples/generate-ppt-style-html.js +611 -0
  15. package/skills/byteplan-html/examples/margin-contribution-analysis.json +152 -0
  16. package/skills/byteplan-html/package.json +18 -0
  17. package/skills/byteplan-html/scripts/generate_html.js +517 -0
  18. package/skills/byteplan-ppt/SKILL.md +394 -0
  19. package/skills/byteplan-ppt/examples/margin-contribution-analysis.json +152 -0
  20. package/skills/byteplan-ppt/package.json +16 -0
  21. package/skills/byteplan-ppt/pnpm-lock.yaml +138 -0
  22. package/skills/byteplan-ppt/scripts/check_ppt_overlap.js +318 -0
  23. package/skills/byteplan-ppt/scripts/generate_ppt.js +680 -0
  24. package/skills/byteplan-video/SKILL.md +606 -0
  25. package/skills/byteplan-video/examples/sample-video-data.json +82 -0
  26. package/skills/byteplan-video/remotion-project/package.json +22 -0
  27. package/skills/byteplan-video/remotion-project/pnpm-lock.yaml +1646 -0
  28. package/skills/byteplan-video/remotion-project/remotion.config.ts +6 -0
  29. package/skills/byteplan-video/remotion-project/scene_durations.json +32 -0
  30. package/skills/byteplan-video/remotion-project/scripts/generate_audio.js +279 -0
  31. package/skills/byteplan-video/remotion-project/src/DynamicReport.tsx +172 -0
  32. package/skills/byteplan-video/remotion-project/src/Root.tsx +51 -0
  33. package/skills/byteplan-video/remotion-project/src/SalesReport.tsx +107 -0
  34. package/skills/byteplan-video/remotion-project/src/index.tsx +4 -0
  35. package/skills/byteplan-video/remotion-project/src/scenes/ChartSlide.tsx +201 -0
  36. package/skills/byteplan-video/remotion-project/src/scenes/CoverSlide.tsx +61 -0
  37. package/skills/byteplan-video/remotion-project/src/scenes/EndSlide.tsx +60 -0
  38. package/skills/byteplan-video/remotion-project/src/scenes/InsightSlide.tsx +101 -0
  39. package/skills/byteplan-video/remotion-project/src/scenes/KpiSlide.tsx +84 -0
  40. package/skills/byteplan-video/remotion-project/src/scenes/RecommendationSlide.tsx +100 -0
  41. package/skills/byteplan-video/remotion-project/tsconfig.json +13 -0
  42. package/skills/byteplan-video/remotion-project/video_data.json +76 -0
  43. package/skills/byteplan-video/scripts/generate_video.js +270 -0
  44. package/skills/byteplan-video/templates/package.json +31 -0
  45. package/skills/byteplan-video/templates/pnpm-lock.yaml +2200 -0
  46. package/skills/byteplan-video/templates/remotion.config.ts +9 -0
  47. package/skills/byteplan-video/templates/scripts/generate-audio.ts +55 -0
  48. package/skills/byteplan-video/templates/src/components/BarChartScene.tsx +153 -0
  49. package/skills/byteplan-video/templates/src/components/InsightScene.tsx +135 -0
  50. package/skills/byteplan-video/templates/src/components/LineChartScene.tsx +214 -0
  51. package/skills/byteplan-video/templates/src/components/SceneFactory.tsx +34 -0
  52. package/skills/byteplan-video/templates/src/components/SummaryScene.tsx +155 -0
  53. package/skills/byteplan-video/templates/src/components/TitleScene.tsx +130 -0
  54. package/skills/byteplan-video/templates/src/compositions/AnalysisVideo.tsx +39 -0
  55. package/skills/byteplan-video/templates/src/index.tsx +28 -0
  56. package/skills/byteplan-video/templates/src/register-root.tsx +4 -0
  57. package/skills/byteplan-video/templates/src/storyboard/types.ts +46 -0
  58. package/skills/byteplan-video/templates/tsconfig.json +17 -0
  59. package/skills/byteplan-video/templates/tsconfig.scripts.json +13 -0
  60. package/skills/byteplan-word/SKILL.md +233 -0
  61. package/skills/byteplan-word/package.json +12 -0
  62. package/skills/byteplan-word/pnpm-lock.yaml +120 -0
  63. package/skills/byteplan-word/scripts/generate_word.js +548 -0
  64. package/src/cli.js +4 -0
  65. package/src/commands/skills.js +279 -0
@@ -0,0 +1,6 @@
1
+ import { Config } from "@remotion/cli/config";
2
+
3
+ Config.setVideoImageFormat("jpeg");
4
+ Config.setOverwriteOutput(true);
5
+
6
+ export default Config;
@@ -0,0 +1,32 @@
1
+ [
2
+ {
3
+ "id": "scene_1",
4
+ "duration": 6,
5
+ "audioFile": "scene_1.mp3"
6
+ },
7
+ {
8
+ "id": "scene_2",
9
+ "duration": 16,
10
+ "audioFile": "scene_2.mp3"
11
+ },
12
+ {
13
+ "id": "scene_3",
14
+ "duration": 8,
15
+ "audioFile": "scene_3.mp3"
16
+ },
17
+ {
18
+ "id": "scene_4",
19
+ "duration": 23,
20
+ "audioFile": "scene_4.mp3"
21
+ },
22
+ {
23
+ "id": "scene_5",
24
+ "duration": 9,
25
+ "audioFile": "scene_5.mp3"
26
+ },
27
+ {
28
+ "id": "scene_6",
29
+ "duration": 2,
30
+ "audioFile": "scene_6.mp3"
31
+ }
32
+ ]
@@ -0,0 +1,279 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+
5
+ const videoData = JSON.parse(fs.readFileSync(path.join(__dirname, '../video_data.json'), 'utf-8'));
6
+ const audioDir = path.join(__dirname, '../public/audio');
7
+
8
+ // 确保音频目录存在
9
+ if (!fs.existsSync(audioDir)) {
10
+ fs.mkdirSync(audioDir, { recursive: true });
11
+ }
12
+
13
+ /**
14
+ * 方式 1: 使用 node-edge-tts npm 包(推荐)
15
+ * 返回 { success: boolean, duration: number|null }
16
+ */
17
+ async function generateWithNpmPackage(text, outputPath, voice = 'zh-CN-XiaoxiaoNeural') {
18
+ try {
19
+ const { EdgeTTS } = require('node-edge-tts');
20
+
21
+ // 生成字幕文件路径(node-edge-tts 保存为 {audio}.json)
22
+ const subtitlePath = outputPath + '.json';
23
+
24
+ const tts = new EdgeTTS({
25
+ voice: voice,
26
+ lang: 'zh-CN',
27
+ outputFormat: 'audio-24khz-48kbitrate-mono-mp3',
28
+ rate: '+0%',
29
+ pitch: '+0Hz',
30
+ saveSubtitles: true // 保存字幕文件,用于获取时长
31
+ });
32
+
33
+ await tts.ttsPromise(text, outputPath);
34
+
35
+ // 从字幕文件获取时长
36
+ let duration = null;
37
+ if (fs.existsSync(subtitlePath)) {
38
+ try {
39
+ const subtitles = JSON.parse(fs.readFileSync(subtitlePath, 'utf-8'));
40
+ // 最后一个词的 end 时间就是总时长(毫秒)
41
+ if (Array.isArray(subtitles) && subtitles.length > 0) {
42
+ const lastItem = subtitles[subtitles.length - 1];
43
+ duration = lastItem.end / 1000; // 转换为秒
44
+ }
45
+ // 删除字幕文件(不需要保留)
46
+ fs.unlinkSync(subtitlePath);
47
+ } catch (e) {
48
+ // 字幕解析失败,返回 null,后续用 ffprobe
49
+ }
50
+ }
51
+
52
+ return { success: true, duration };
53
+ } catch (error) {
54
+ return { success: false, duration: null };
55
+ }
56
+ }
57
+
58
+ /**
59
+ * 方式 2: 使用 Python edge-tts 命令行工具(备用)
60
+ */
61
+ function detectPythonEdgeTts() {
62
+ const commands = ['edge-tts', 'python -m edge_tts', 'python3 -m edge_tts'];
63
+
64
+ for (const cmd of commands) {
65
+ try {
66
+ execSync(`${cmd} --version`, { stdio: 'pipe', timeout: 5000 });
67
+ return cmd;
68
+ } catch (e) {
69
+ // 继续尝试下一个命令
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+
75
+ function generateWithPythonEdgeTts(text, outputPath, edgeTtsCmd, voice = 'zh-CN-XiaoxiaoNeural') {
76
+ // 使用单引号包裹文本,转义内部单引号
77
+ const escapedText = "'" + text.replace(/'/g, "'\\''") + "'";
78
+
79
+ const command = `${edgeTtsCmd} --voice "${voice}" --text ${escapedText} --write-media "${outputPath}"`;
80
+
81
+ execSync(command, {
82
+ stdio: 'pipe',
83
+ timeout: 30000,
84
+ shell: process.platform === 'win32' ? 'cmd.exe' : undefined
85
+ });
86
+
87
+ return fs.existsSync(outputPath);
88
+ }
89
+
90
+ /**
91
+ * 使用 ffprobe 获取音频时长(备用方案)
92
+ */
93
+ function getAudioDurationWithFfprobe(audioPath) {
94
+ try {
95
+ const durationStr = execSync(
96
+ `ffprobe -i "${audioPath}" -show_entries format=duration -v quiet -of csv="p=0"`,
97
+ { encoding: 'utf-8', shell: process.platform === 'win32' ? 'cmd.exe' : undefined }
98
+ ).toString().trim();
99
+
100
+ return parseFloat(durationStr);
101
+ } catch (error) {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * 检测 ffprobe
108
+ */
109
+ function checkFfprobe() {
110
+ try {
111
+ execSync('ffprobe -version', { stdio: 'pipe' });
112
+ return true;
113
+ } catch (e) {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * 生成单个场景的语音
120
+ */
121
+ async function generateSceneAudio(scene, index, useNpm, pythonCmd, hasFfprobe) {
122
+ if (!scene.narration) {
123
+ console.log(`场景 ${index + 1}: 无解说文本,使用默认时长 3 秒`);
124
+ return { id: scene.id, duration: 3 };
125
+ }
126
+
127
+ const audioPath = path.join(audioDir, `${scene.id}.mp3`);
128
+
129
+ console.log(`\n生成语音: ${scene.id}`);
130
+ console.log(`文本: ${scene.narration.substring(0, 50)}...`);
131
+
132
+ let success = false;
133
+ let duration = null;
134
+
135
+ // 优先使用 npm 包
136
+ if (useNpm) {
137
+ console.log('使用 node-edge-tts npm 包...');
138
+ const result = await generateWithNpmPackage(scene.narration, audioPath);
139
+ success = result.success;
140
+ duration = result.duration;
141
+
142
+ if (!success) {
143
+ console.log('npm 包不可用,尝试 Python edge-tts...');
144
+ } else if (duration) {
145
+ console.log(`✅ 从字幕获取时长: ${duration.toFixed(2)} 秒`);
146
+ }
147
+ }
148
+
149
+ // 回退到 Python edge-tts
150
+ if (!success && pythonCmd) {
151
+ console.log(`使用 Python edge-tts (${pythonCmd})...`);
152
+ try {
153
+ success = generateWithPythonEdgeTts(scene.narration, audioPath, pythonCmd);
154
+ } catch (error) {
155
+ console.error(`Python edge-tts 失败: ${error.message}`);
156
+ }
157
+ }
158
+
159
+ if (!success) {
160
+ console.error('❌ 所有 TTS 方式都失败');
161
+ return { id: scene.id, duration: 5 };
162
+ }
163
+
164
+ // 检查文件是否生成
165
+ if (!fs.existsSync(audioPath)) {
166
+ console.error('❌ 音频文件未生成');
167
+ return { id: scene.id, duration: 5 };
168
+ }
169
+
170
+ // 如果 npm 包没有返回时长,尝试 ffprobe
171
+ if (duration === null) {
172
+ if (hasFfprobe) {
173
+ duration = getAudioDurationWithFfprobe(audioPath);
174
+ if (duration !== null) {
175
+ console.log(`✅ 从 ffprobe 获取时长: ${duration.toFixed(2)} 秒`);
176
+ }
177
+ }
178
+
179
+ // 如果还是没有时长,使用默认值
180
+ if (duration === null) {
181
+ console.log('⚠️ 无法获取音频时长,使用默认 5 秒');
182
+ duration = 5;
183
+ }
184
+ }
185
+
186
+ // 添加 0.5 秒缓冲
187
+ return {
188
+ id: scene.id,
189
+ duration: Math.ceil(duration + 0.5),
190
+ audioFile: `${scene.id}.mp3`
191
+ };
192
+ }
193
+
194
+ // 主流程
195
+ async function main() {
196
+ console.log('========================================');
197
+ console.log('BytePlan 视频语音生成工具');
198
+ console.log('========================================\n');
199
+
200
+ // 检测 TTS 工具
201
+ let useNpm = false;
202
+ let pythonCmd = null;
203
+
204
+ // 检测 npm 包
205
+ try {
206
+ require('node-edge-tts');
207
+ useNpm = true;
208
+ console.log('✅ 检测到 node-edge-tts npm 包');
209
+ } catch (e) {
210
+ console.log('⚠️ 未检测到 node-edge-tts npm 包');
211
+ }
212
+
213
+ // 检测 Python edge-tts
214
+ pythonCmd = detectPythonEdgeTts();
215
+ if (pythonCmd) {
216
+ console.log(`✅ 检测到 Python edge-tts: ${pythonCmd}`);
217
+ } else {
218
+ console.log('⚠️ 未检测到 Python edge-tts');
219
+ }
220
+
221
+ // 如果两者都没有
222
+ if (!useNpm && !pythonCmd) {
223
+ console.error('\n❌ 未检测到任何 TTS 工具,请安装其中一种:');
224
+ console.error('');
225
+ console.error('方式 1: 安装 npm 包(推荐,无需额外依赖)');
226
+ console.error(' npm install node-edge-tts');
227
+ console.error(' 或');
228
+ console.error(' pnpm add node-edge-tts');
229
+ console.error('');
230
+ console.error('方式 2: 安装 Python edge-tts');
231
+ console.error(' pip install edge-tts');
232
+ console.error(' 或');
233
+ console.error(' pip3 install edge-tts');
234
+ process.exit(1);
235
+ }
236
+
237
+ // 检测 ffprobe(可选,仅 Python edge-tts 需要)
238
+ const hasFfprobe = checkFfprobe();
239
+ if (hasFfprobe) {
240
+ console.log('✅ 检测到 ffprobe(备用)');
241
+ } else {
242
+ if (!useNpm && pythonCmd) {
243
+ // 只有在使用 Python edge-tts 时才需要 ffprobe
244
+ console.error('❌ 使用 Python edge-tts 需要安装 ffprobe:');
245
+ console.error(' Windows: choco install ffmpeg');
246
+ console.error(' macOS: brew install ffmpeg');
247
+ console.error(' Linux: sudo apt install ffmpeg');
248
+ console.error('');
249
+ console.error('或者使用 npm 包(node-edge-tts),无需 ffprobe');
250
+ process.exit(1);
251
+ }
252
+ console.log('⚠️ 未检测到 ffprobe(npm 包模式下不影响使用)');
253
+ }
254
+
255
+ console.log('\n开始生成语音...\n');
256
+
257
+ // 生成所有场景的语音
258
+ const sceneDurations = [];
259
+ for (let i = 0; i < videoData.scenes.length; i++) {
260
+ const result = await generateSceneAudio(videoData.scenes[i], i, useNpm, pythonCmd, hasFfprobe);
261
+ sceneDurations.push(result);
262
+ }
263
+
264
+ // 保存时长配置
265
+ const durationsPath = path.join(__dirname, '../scene_durations.json');
266
+ fs.writeFileSync(durationsPath, JSON.stringify(sceneDurations, null, 2));
267
+
268
+ console.log('\n========================================');
269
+ console.log('所有语音生成完成!');
270
+ console.log('场景时长配置已保存到 scene_durations.json');
271
+ console.log('\n时长汇总:');
272
+ sceneDurations.forEach((s, i) => {
273
+ console.log(` ${i + 1}. ${s.id}: ${s.duration} 秒`);
274
+ });
275
+ const totalDuration = sceneDurations.reduce((sum, s) => sum + s.duration, 0);
276
+ console.log(`\n总时长: ${totalDuration} 秒 (${Math.floor(totalDuration / 60)}分${totalDuration % 60}秒)`);
277
+ }
278
+
279
+ main().catch(console.error);
@@ -0,0 +1,172 @@
1
+ import {
2
+ AbsoluteFill,
3
+ useVideoConfig,
4
+ Audio,
5
+ staticFile,
6
+ Sequence,
7
+ } from "remotion";
8
+ import { CoverSlide } from "./scenes/CoverSlide";
9
+ import { ChartSlide } from "./scenes/ChartSlide";
10
+ import { InsightSlide } from "./scenes/InsightSlide";
11
+ import { RecommendationSlide } from "./scenes/RecommendationSlide";
12
+ import { EndSlide } from "./scenes/EndSlide";
13
+
14
+ interface SlideData {
15
+ id: string;
16
+ type: string;
17
+ title: string;
18
+ subtitle?: string;
19
+ duration: number;
20
+ audioFile: string;
21
+ startFrame: number;
22
+ endFrame: number;
23
+ highlights?: string[];
24
+ points?: string[];
25
+ source?: string;
26
+ dataKey?: string;
27
+ visualConfig?: {
28
+ highlights?: string[];
29
+ points?: string[];
30
+ source?: string;
31
+ dataKey?: string;
32
+ title?: string;
33
+ subtitle?: string;
34
+ };
35
+ }
36
+
37
+ interface ChartData {
38
+ [key: string]: Array<{ name: string; value: number }>;
39
+ }
40
+
41
+ interface DynamicReportProps {
42
+ slides: SlideData[];
43
+ chartData?: ChartData;
44
+ period?: string;
45
+ }
46
+
47
+ /**
48
+ * 将 highlights 字符串数组转换为 insights 格式
49
+ */
50
+ function parseHighlights(highlights: string[]): Array<{ title: string; content: string; warning?: boolean }> {
51
+ return highlights.map(h => {
52
+ // 检测是否包含警告标志
53
+ const warning = h.includes('⚠️') || h.includes('风险') || h.includes('异常');
54
+
55
+ // 移除 emoji,提取标题和内容
56
+ const cleanText = h.replace(/[🥇🥈🥉⚠️]/g, '').trim();
57
+
58
+ // 尝试分割标题和内容
59
+ const colonIndex = cleanText.indexOf(':');
60
+ if (colonIndex > 0) {
61
+ const title = cleanText.substring(0, colonIndex).trim();
62
+ const content = cleanText.substring(colonIndex + 1).trim();
63
+ return { title, content, warning };
64
+ }
65
+
66
+ // 如果没有冒号,整段作为内容
67
+ return { title: '', content: cleanText, warning };
68
+ });
69
+ }
70
+
71
+ export const DynamicReport: React.FC<DynamicReportProps> = ({ slides, chartData, period }) => {
72
+ const { fps } = useVideoConfig();
73
+
74
+ // 根据场景类型渲染对应组件
75
+ const renderScene = (slide: SlideData, index: number) => {
76
+ switch (slide.type) {
77
+ case 'title':
78
+ return (
79
+ <CoverSlide
80
+ title={slide.title}
81
+ subtitle={slide.subtitle || ''}
82
+ />
83
+ );
84
+
85
+ case 'insight':
86
+ // 支持 highlights 字符串数组(顶层或 visualConfig 内)
87
+ const rawHighlights = slide.highlights || slide.visualConfig?.highlights || [];
88
+ const insights = rawHighlights.length > 0
89
+ ? parseHighlights(rawHighlights)
90
+ : [];
91
+ return (
92
+ <InsightSlide
93
+ title={slide.title}
94
+ insights={insights}
95
+ />
96
+ );
97
+
98
+ case 'bar_chart':
99
+ case 'line_chart':
100
+ case 'chart':
101
+ // 从 chartData 获取图表数据,优先使用 dataKey
102
+ const chartKey = slide.dataKey || slide.visualConfig?.dataKey || Object.keys(chartData || {})[0] || 'default';
103
+ const data = chartData?.[chartKey] || [];
104
+
105
+ // 数据格式化:转换为合适单位
106
+ const formattedData = data.map(d => ({
107
+ name: d.name,
108
+ value: typeof d.value === 'number' && d.value > 10000
109
+ ? Math.round(d.value / 10000) // 转换为万元
110
+ : d.value
111
+ }));
112
+
113
+ return (
114
+ <ChartSlide
115
+ title={slide.title}
116
+ chartType={slide.type === 'line_chart' ? 'line' : 'bar'}
117
+ data={formattedData}
118
+ />
119
+ );
120
+
121
+ case 'summary':
122
+ const rawPoints = slide.points || slide.visualConfig?.points || [];
123
+ const rawSource = slide.source || slide.visualConfig?.source;
124
+ return (
125
+ <RecommendationSlide
126
+ title={slide.title}
127
+ recommendations={rawPoints}
128
+ source={rawSource}
129
+ />
130
+ );
131
+
132
+ case 'end':
133
+ return (
134
+ <EndSlide
135
+ title="谢谢观看"
136
+ subtitle={slide.title}
137
+ />
138
+ );
139
+
140
+ default:
141
+ // 默认渲染为洞察卡片
142
+ return (
143
+ <InsightSlide
144
+ title={slide.title}
145
+ insights={slide.highlights ? parseHighlights(slide.highlights) : [{ title: '', content: '' }]}
146
+ />
147
+ );
148
+ }
149
+ };
150
+
151
+ return (
152
+ <AbsoluteFill
153
+ style={{
154
+ backgroundColor: "#1a1a2e",
155
+ fontFamily: "'Microsoft YaHei', '微软雅黑', Arial, sans-serif",
156
+ }}
157
+ >
158
+ {slides.map((slide, index) => (
159
+ <Sequence
160
+ key={slide.id}
161
+ from={slide.startFrame}
162
+ durationInFrames={slide.duration * fps}
163
+ >
164
+ {renderScene(slide, index)}
165
+ {slide.audioFile && (
166
+ <Audio src={staticFile(`audio/${slide.audioFile}`)} />
167
+ )}
168
+ </Sequence>
169
+ ))}
170
+ </AbsoluteFill>
171
+ );
172
+ };
@@ -0,0 +1,51 @@
1
+ import { Composition, staticFile } from "remotion";
2
+ import { DynamicReport } from "./DynamicReport";
3
+ import videoData from "../video_data.json";
4
+ import sceneDurations from "../scene_durations.json";
5
+
6
+ // 计算总帧数
7
+ const fps = 30;
8
+
9
+ // 合并场景时长信息
10
+ const slidesWithData = videoData.scenes.map((scene, index) => {
11
+ const durationInfo = sceneDurations.find(s => s.id === scene.id);
12
+ return {
13
+ ...scene,
14
+ duration: durationInfo?.duration || scene.duration || 10,
15
+ audioFile: durationInfo?.audioFile || `${scene.id}.mp3`
16
+ };
17
+ });
18
+
19
+ // 计算总帧数和每个场景的帧范围
20
+ let currentFrame = 0;
21
+ const slideData = slidesWithData.map((s) => {
22
+ const startFrame = currentFrame;
23
+ currentFrame += s.duration * fps;
24
+ return {
25
+ ...s,
26
+ startFrame,
27
+ endFrame: currentFrame
28
+ };
29
+ });
30
+
31
+ const totalFrames = currentFrame;
32
+
33
+ export const RemotionRoot: React.FC = () => {
34
+ return (
35
+ <>
36
+ <Composition
37
+ id="SalesReport"
38
+ component={DynamicReport}
39
+ durationInFrames={totalFrames}
40
+ fps={fps}
41
+ width={1920}
42
+ height={1080}
43
+ defaultProps={{
44
+ slides: slideData,
45
+ chartData: videoData.chartData,
46
+ period: videoData.period
47
+ }}
48
+ />
49
+ </>
50
+ );
51
+ };
@@ -0,0 +1,107 @@
1
+ import {
2
+ AbsoluteFill,
3
+ useCurrentFrame,
4
+ useVideoConfig,
5
+ Audio,
6
+ staticFile,
7
+ Sequence,
8
+ } from "remotion";
9
+ import { CoverSlide } from "./scenes/CoverSlide";
10
+ import { KpiSlide } from "./scenes/KpiSlide";
11
+ import { ChartSlide } from "./scenes/ChartSlide";
12
+ import { InsightSlide } from "./scenes/InsightSlide";
13
+ import { RecommendationSlide } from "./scenes/RecommendationSlide";
14
+ import { EndSlide } from "./scenes/EndSlide";
15
+
16
+ interface SlideData {
17
+ id: string;
18
+ duration: number;
19
+ audioFile: string;
20
+ startFrame: number;
21
+ endFrame: number;
22
+ }
23
+
24
+ interface SalesReportProps {
25
+ slides: SlideData[];
26
+ }
27
+
28
+ export const SalesReport: React.FC<SalesReportProps> = ({ slides }) => {
29
+ const { fps } = useVideoConfig();
30
+
31
+ return (
32
+ <AbsoluteFill
33
+ style={{
34
+ backgroundColor: "#1a1a2e",
35
+ fontFamily: "'Microsoft YaHei', '微软雅黑', Arial, sans-serif",
36
+ }}
37
+ >
38
+ {/* 场景1: 封面 */}
39
+ <Sequence from={0} durationInFrames={slides[0]?.endFrame || 180}>
40
+ <CoverSlide title="边际贡献分析报告" subtitle="找出贡献最大的三个要素 · 跨境电商" />
41
+ <Audio src={staticFile(`audio/${slides[0]?.audioFile}`)} />
42
+ </Sequence>
43
+
44
+ {/* 场景2: 核心发现 */}
45
+ <Sequence from={slides[0]?.endFrame || 180} durationInFrames={slides[1]?.duration * fps || 450}>
46
+ <InsightSlide
47
+ title="核心发现"
48
+ insights={[
49
+ { title: "🥇 第一名:天猫", content: "边际贡献 1,627万元,贡献率 68%" },
50
+ { title: "🥈 第二名:CD120P", content: "边际贡献 563万元,贡献率 74%" },
51
+ { title: "🥉 第三名:美国", content: "边际贡献 498万元,贡献率 65%" }
52
+ ]}
53
+ />
54
+ <Audio src={staticFile(`audio/${slides[1]?.audioFile}`)} />
55
+ </Sequence>
56
+
57
+ {/* 场景3: 边际贡献对比图 */}
58
+ <Sequence from={slides[1]?.endFrame || 630} durationInFrames={slides[2]?.duration * fps || 240}>
59
+ <ChartSlide
60
+ title="边际贡献对比(TOP5)"
61
+ chartType="bar"
62
+ data={[
63
+ { name: "天猫", value: 1627 },
64
+ { name: "CD120P", value: 563 },
65
+ { name: "美国", value: 498 },
66
+ { name: "Amazon", value: 279 },
67
+ { name: "京东", value: 156 }
68
+ ]}
69
+ />
70
+ <Audio src={staticFile(`audio/${slides[2]?.audioFile}`)} />
71
+ </Sequence>
72
+
73
+ {/* 场景4: 关键洞察 */}
74
+ <Sequence from={slides[2]?.endFrame || 870} durationInFrames={slides[3]?.duration * fps || 540}>
75
+ <InsightSlide
76
+ title="关键洞察"
77
+ insights={[
78
+ { title: "天猫渠道表现卓越", content: "贡献占比68%,是最大收入来源" },
79
+ { title: "CD120P盈利效率最高", content: "贡献率74%,变动成本控制得当" },
80
+ { title: "美国市场规模最大", content: "收入3,543万元最高,贡献率65%" },
81
+ { title: "⚠️ 天猫变动成本偏高", content: "需排查运营成本构成,优化空间大", warning: true }
82
+ ]}
83
+ />
84
+ <Audio src={staticFile(`audio/${slides[3]?.audioFile}`)} />
85
+ </Sequence>
86
+
87
+ {/* 场景5: 行动建议 */}
88
+ <Sequence from={slides[3]?.endFrame || 1410} durationInFrames={slides[4]?.duration * fps || 270}>
89
+ <RecommendationSlide
90
+ title="行动建议"
91
+ recommendations={[
92
+ "加大CD120P产能,贡献率74%的产品应获得更多资源",
93
+ "优化天猫运营成本,降低变动成本提升贡献率",
94
+ "建立月度边际贡献监控机制,及时发现问题"
95
+ ]}
96
+ source="BytePlan 数据平台 · 跨境电商租户"
97
+ />
98
+ <Audio src={staticFile(`audio/${slides[4]?.audioFile}`)} />
99
+ </Sequence>
100
+
101
+ {/* 场景6: 结尾 */}
102
+ <Sequence from={slides[4]?.endFrame || 1680} durationInFrames={slides[5]?.duration * fps || 300}>
103
+ <EndSlide title="谢谢观看" subtitle="边际贡献分析报告 · BytePlan" />
104
+ </Sequence>
105
+ </AbsoluteFill>
106
+ );
107
+ };
@@ -0,0 +1,4 @@
1
+ import { registerRoot } from "remotion";
2
+ import { RemotionRoot } from "./Root";
3
+
4
+ registerRoot(RemotionRoot);