@lmliheng/lesson-video 0.0.0-stage → 0.3.2
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.
- package/LICENSE +201 -0
- package/README.md +50 -2
- package/icon.svg +7 -0
- package/package.json +30 -3
- package/plugin.json +9 -0
- package/skills/lesson-video/SKILL.md +172 -0
- package/skills/lesson-video/example/covers.json +14 -0
- package/skills/lesson-video/example/script.json +53 -0
- package/skills/lesson-video/kit/build.py +78 -0
- package/skills/lesson-video/kit/cover.html +257 -0
- package/skills/lesson-video/kit/cover.mjs +64 -0
- package/skills/lesson-video/kit/deck.html +632 -0
- package/skills/lesson-video/kit/render.mjs +169 -0
- package/skills/lesson-video/kit/tts.py +223 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Kaggle 房价预测 · 封面</title>
|
|
6
|
+
<style>
|
|
7
|
+
/* 竖屏封面:与课件同一套视觉语言(纯黑 + 点阵 + 单一蓝强调),一帧定格、无动画 */
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #000;
|
|
10
|
+
--surface: #0d0d0d;
|
|
11
|
+
--border: #1f1f1f;
|
|
12
|
+
--border-2: #303030;
|
|
13
|
+
--fg: #f3f4f6;
|
|
14
|
+
--fg-muted: #9ca3af;
|
|
15
|
+
--fg-faint: #6b7280;
|
|
16
|
+
--brand: #8ab4f8;
|
|
17
|
+
--brand-deep: #4285f4;
|
|
18
|
+
--brand-tint: #1a2b45;
|
|
19
|
+
}
|
|
20
|
+
* {
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
-webkit-font-smoothing: antialiased;
|
|
23
|
+
}
|
|
24
|
+
html,
|
|
25
|
+
body {
|
|
26
|
+
margin: 0;
|
|
27
|
+
padding: 0;
|
|
28
|
+
width: 1080px;
|
|
29
|
+
height: 100%;
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
background: var(--bg);
|
|
32
|
+
color: var(--fg);
|
|
33
|
+
font-family:
|
|
34
|
+
"Microsoft YaHei", "PingFang SC", ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#grid {
|
|
38
|
+
position: absolute;
|
|
39
|
+
inset: 0;
|
|
40
|
+
background-image: radial-gradient(rgba(138, 180, 248, 0.12) 1.8px, transparent 1.8px);
|
|
41
|
+
background-size: 76px 76px;
|
|
42
|
+
}
|
|
43
|
+
#num {
|
|
44
|
+
position: absolute;
|
|
45
|
+
right: -70px;
|
|
46
|
+
bottom: -190px;
|
|
47
|
+
font-size: 900px;
|
|
48
|
+
font-weight: 700;
|
|
49
|
+
line-height: 0.78;
|
|
50
|
+
color: rgba(66, 133, 244, 0.11);
|
|
51
|
+
letter-spacing: -0.06em;
|
|
52
|
+
user-select: none;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#top {
|
|
56
|
+
position: absolute;
|
|
57
|
+
left: 96px;
|
|
58
|
+
right: 150px;
|
|
59
|
+
top: 104px;
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
justify-content: space-between;
|
|
63
|
+
}
|
|
64
|
+
#brand {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: 18px;
|
|
68
|
+
font-size: 32px;
|
|
69
|
+
color: var(--fg-faint);
|
|
70
|
+
letter-spacing: 0.02em;
|
|
71
|
+
}
|
|
72
|
+
#brand .mark {
|
|
73
|
+
width: 44px;
|
|
74
|
+
height: 44px;
|
|
75
|
+
border-radius: 12px 12px 12px 5px;
|
|
76
|
+
background: var(--brand-deep);
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
font-size: 28px;
|
|
81
|
+
font-weight: 700;
|
|
82
|
+
color: #08111f;
|
|
83
|
+
}
|
|
84
|
+
#wm {
|
|
85
|
+
font-size: 26px;
|
|
86
|
+
color: var(--fg-faint);
|
|
87
|
+
opacity: 0.55;
|
|
88
|
+
font-family: Consolas, "Cascadia Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#mid {
|
|
92
|
+
position: absolute;
|
|
93
|
+
left: 96px;
|
|
94
|
+
right: 120px;
|
|
95
|
+
top: 0;
|
|
96
|
+
bottom: 0;
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
justify-content: center;
|
|
100
|
+
gap: 0;
|
|
101
|
+
}
|
|
102
|
+
.badge {
|
|
103
|
+
align-self: flex-start;
|
|
104
|
+
background: var(--brand-tint);
|
|
105
|
+
color: var(--brand);
|
|
106
|
+
font-size: 38px;
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
letter-spacing: 0.06em;
|
|
109
|
+
padding: 14px 34px;
|
|
110
|
+
border-radius: 9999px;
|
|
111
|
+
margin-bottom: 52px;
|
|
112
|
+
}
|
|
113
|
+
.title {
|
|
114
|
+
font-size: 134px;
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
letter-spacing: -0.045em;
|
|
117
|
+
line-height: 1.12;
|
|
118
|
+
margin: 0;
|
|
119
|
+
}
|
|
120
|
+
.title .ln {
|
|
121
|
+
display: block;
|
|
122
|
+
}
|
|
123
|
+
.title .hi {
|
|
124
|
+
color: var(--brand);
|
|
125
|
+
}
|
|
126
|
+
.rule {
|
|
127
|
+
width: 230px;
|
|
128
|
+
height: 7px;
|
|
129
|
+
background: var(--brand-deep);
|
|
130
|
+
border-radius: 4px;
|
|
131
|
+
margin: 56px 0 44px;
|
|
132
|
+
}
|
|
133
|
+
.sub {
|
|
134
|
+
font-size: 48px;
|
|
135
|
+
line-height: 1.42;
|
|
136
|
+
color: var(--fg-muted);
|
|
137
|
+
margin: 0;
|
|
138
|
+
}
|
|
139
|
+
.sub .ln {
|
|
140
|
+
display: block;
|
|
141
|
+
}
|
|
142
|
+
.pills {
|
|
143
|
+
display: flex;
|
|
144
|
+
flex-wrap: wrap;
|
|
145
|
+
gap: 18px;
|
|
146
|
+
margin-top: 54px;
|
|
147
|
+
}
|
|
148
|
+
.pill {
|
|
149
|
+
border: 1px solid var(--border-2);
|
|
150
|
+
border-radius: 9999px;
|
|
151
|
+
padding: 14px 32px;
|
|
152
|
+
font-size: 34px;
|
|
153
|
+
color: var(--fg-muted);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#foot {
|
|
157
|
+
position: absolute;
|
|
158
|
+
left: 96px;
|
|
159
|
+
right: 150px;
|
|
160
|
+
bottom: 110px;
|
|
161
|
+
display: flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
justify-content: space-between;
|
|
164
|
+
font-size: 36px;
|
|
165
|
+
color: var(--fg-muted);
|
|
166
|
+
}
|
|
167
|
+
#foot .cta {
|
|
168
|
+
background: var(--fg);
|
|
169
|
+
color: #111827;
|
|
170
|
+
font-weight: 600;
|
|
171
|
+
border-radius: 10px;
|
|
172
|
+
padding: 18px 42px;
|
|
173
|
+
font-size: 34px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* 主页 3:4 版:内容居中不变,只把字号与留白收一点,避免上下被裁 */
|
|
177
|
+
.h34 .title {
|
|
178
|
+
font-size: 118px;
|
|
179
|
+
}
|
|
180
|
+
.h34 .sub {
|
|
181
|
+
font-size: 44px;
|
|
182
|
+
}
|
|
183
|
+
.h34 .badge {
|
|
184
|
+
font-size: 34px;
|
|
185
|
+
margin-bottom: 44px;
|
|
186
|
+
}
|
|
187
|
+
.h34 .rule {
|
|
188
|
+
margin: 46px 0 38px;
|
|
189
|
+
}
|
|
190
|
+
.h34 .pills {
|
|
191
|
+
margin-top: 46px;
|
|
192
|
+
}
|
|
193
|
+
.h34 #foot {
|
|
194
|
+
bottom: 80px;
|
|
195
|
+
}
|
|
196
|
+
.h34 #num {
|
|
197
|
+
font-size: 760px;
|
|
198
|
+
bottom: -160px;
|
|
199
|
+
}
|
|
200
|
+
</style>
|
|
201
|
+
</head>
|
|
202
|
+
<body>
|
|
203
|
+
<div id="grid"></div>
|
|
204
|
+
<div id="num"></div>
|
|
205
|
+
<div id="top">
|
|
206
|
+
<div id="brand"><span class="mark" id="brand-mark"></span><span id="brand-text"></span></div>
|
|
207
|
+
<div id="wm"></div>
|
|
208
|
+
</div>
|
|
209
|
+
<div id="mid">
|
|
210
|
+
<div class="badge" id="badge"></div>
|
|
211
|
+
<h1 class="title" id="title"></h1>
|
|
212
|
+
<div class="rule"></div>
|
|
213
|
+
<p class="sub" id="sub"></p>
|
|
214
|
+
<div class="pills" id="pills"></div>
|
|
215
|
+
</div>
|
|
216
|
+
<div id="foot">
|
|
217
|
+
<span id="foot-left"></span>
|
|
218
|
+
<span class="cta" id="cta"></span>
|
|
219
|
+
</div>
|
|
220
|
+
<script>
|
|
221
|
+
"use strict";
|
|
222
|
+
const esc = (s) =>
|
|
223
|
+
String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
224
|
+
window.__init = function (c, size) {
|
|
225
|
+
if (size === "3x4") document.body.classList.add("h34");
|
|
226
|
+
const brand = c.brand || "";
|
|
227
|
+
document.getElementById("brand-text").textContent = brand;
|
|
228
|
+
// 台标字母 / 右上角水印 / 右下角 CTA 都从数据来:没给就自动取首字、或整块隐藏
|
|
229
|
+
document.getElementById("brand-mark").textContent =
|
|
230
|
+
c.mark || brand.trim().slice(0, 1) || "▶";
|
|
231
|
+
const wm = document.getElementById("wm");
|
|
232
|
+
wm.textContent = c.wm || "";
|
|
233
|
+
wm.style.display = c.wm ? "" : "none";
|
|
234
|
+
const cta = document.getElementById("cta");
|
|
235
|
+
cta.textContent = c.cta || "";
|
|
236
|
+
cta.style.display = c.cta ? "" : "none";
|
|
237
|
+
document.getElementById("badge").textContent =
|
|
238
|
+
"第 " + c.no + " 集" + (c.tag ? " · " + c.tag : "");
|
|
239
|
+
document.getElementById("title").innerHTML = c.titleLines
|
|
240
|
+
.map(
|
|
241
|
+
(l, i) =>
|
|
242
|
+
`<span class="ln${i === c.titleLines.length - 1 && c.hiLast !== false ? " hi" : ""}">${esc(l)}</span>`,
|
|
243
|
+
)
|
|
244
|
+
.join("");
|
|
245
|
+
document.getElementById("sub").innerHTML = c.subLines
|
|
246
|
+
.map((l) => `<span class="ln">${esc(l)}</span>`)
|
|
247
|
+
.join("");
|
|
248
|
+
document.getElementById("pills").innerHTML = c.pills
|
|
249
|
+
.map((p) => `<span class="pill">${esc(p)}</span>`)
|
|
250
|
+
.join("");
|
|
251
|
+
document.getElementById("num").textContent = c.no;
|
|
252
|
+
document.getElementById("foot-left").textContent = c.foot || "Kaggle 房价预测 · 16 集";
|
|
253
|
+
document.title = c.no + " " + c.titleLines.join("");
|
|
254
|
+
};
|
|
255
|
+
</script>
|
|
256
|
+
</body>
|
|
257
|
+
</html>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 把 covers/covers.json 渲染成抖音封面图:9:16(1080×1920,视频首图)与 3:4(1080×1440,主页封面)。
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* PLAYWRIGHT_MODULE=<pw-core 路径> CHROME_PATH=<chrome.exe> \
|
|
7
|
+
* node lib/cover.mjs --data covers/covers.json --out covers/out [--sizes 1920,1440]
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const pw = require(process.env.PLAYWRIGHT_MODULE || "playwright-core");
|
|
17
|
+
const CHROME = process.env.CHROME_PATH || "";
|
|
18
|
+
|
|
19
|
+
const argv = process.argv.slice(2);
|
|
20
|
+
const arg = (k, d) => {
|
|
21
|
+
const i = argv.indexOf(k);
|
|
22
|
+
return i >= 0 ? argv[i + 1] : d;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const dataFile = path.resolve(arg("--data", "covers/covers.json"));
|
|
26
|
+
const outDir = path.resolve(arg("--out", "covers/out"));
|
|
27
|
+
const sizes = arg("--sizes", "1920,1440")
|
|
28
|
+
.split(",")
|
|
29
|
+
.map((s) => parseInt(s, 10));
|
|
30
|
+
const W = 1080;
|
|
31
|
+
|
|
32
|
+
const covers = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
33
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
34
|
+
|
|
35
|
+
const browser = await pw.chromium.launch({
|
|
36
|
+
executablePath: CHROME || undefined,
|
|
37
|
+
args: [
|
|
38
|
+
"--no-sandbox",
|
|
39
|
+
"--disable-dev-shm-usage",
|
|
40
|
+
"--hide-scrollbars",
|
|
41
|
+
"--force-color-profile=srgb",
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
for (const c of covers) {
|
|
46
|
+
for (const H of sizes) {
|
|
47
|
+
const size = H === 1920 ? "9x16" : `${W}x${H}`;
|
|
48
|
+
const ctx = await browser.newContext({
|
|
49
|
+
viewport: { width: W, height: H },
|
|
50
|
+
deviceScaleFactor: 1,
|
|
51
|
+
});
|
|
52
|
+
const page = await ctx.newPage();
|
|
53
|
+
await page.addInitScript(`window.__COVER__ = ${JSON.stringify(c)};`);
|
|
54
|
+
await page.goto(pathToFileURL(path.join(HERE, "cover.html")).href, { waitUntil: "load" });
|
|
55
|
+
await page.evaluate((s) => window.__init(window.__COVER__, s), H === 1440 ? "3x4" : "9x16");
|
|
56
|
+
await page.evaluate(() => document.fonts.ready);
|
|
57
|
+
const file = path.join(outDir, `${c.id}-${size}.png`);
|
|
58
|
+
await page.screenshot({ path: file });
|
|
59
|
+
await ctx.close();
|
|
60
|
+
console.log(`OK ${file} ${W}x${H}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await browser.close();
|