Skip to content

Commit a93a90f

Browse files
committed
feat: add Start and Stop Streaming image generation and update agent instructions
1 parent f103889 commit a93a90f

6 files changed

Lines changed: 124 additions & 23 deletions

File tree

94.2 KB
Loading
110 KB
Loading

agent.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ npm run generate:recording-title -- _events/2026-01-20/index.md
7474
Recording title image notes:
7575

7676
- The generator reads `title`, `date`, and `speakers` from event front matter.
77-
- It writes `_events/<slug>/Title for Recording.png` and overwrites existing output.
78-
- The background template is `templates/Title for Recording.png`.
77+
- It writes `_events/<slug>/Title for Recording.png`, `_events/<slug>/Start Streaming.png`, and `_events/<slug>/Stop Streaming.png` and overwrites existing outputs.
78+
- Background templates are `templates/Title for Recording.png`, `templates/Start Streaming.png`, and `templates/Stop Streaming.png`.
7979
- Text rendering uses Consolas.
80+
- `Title for Recording.png` renders `.NET Meetup <Month Year>`, then title, then `by <speaker(s)>`.
81+
- `Start Streaming.png` and `Stop Streaming.png` render title and `by <speaker(s)>`.
8082
- Validate correctness by comparing generated outputs for `_events/2026-01-20/Title for Recording.png` and `_events/2025-12-16/Title for Recording.png` with their existing examples; only minor anti-aliasing/font-rendering differences are acceptable.

scripts/generate_recording_title_image.js

Lines changed: 120 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,26 @@ const matter = require('gray-matter');
66
const { chromium } = require('@playwright/test');
77

88
const ROOT_DIR = path.join(__dirname, '..');
9-
const TEMPLATE_PATH = path.join(ROOT_DIR, 'templates', 'Title for Recording.png');
10-
const OUTPUT_FILENAME = 'Title for Recording.png';
9+
const IMAGE_PROFILES = [
10+
{
11+
name: 'recording',
12+
templatePath: path.join(ROOT_DIR, 'templates', 'Title for Recording.png'),
13+
outputFilename: 'Title for Recording.png'
14+
},
15+
{
16+
name: 'start',
17+
templatePath: path.join(ROOT_DIR, 'templates', 'Start Streaming.png'),
18+
outputFilename: 'Start Streaming.png'
19+
},
20+
{
21+
name: 'stop',
22+
templatePath: path.join(ROOT_DIR, 'templates', 'Stop Streaming.png'),
23+
outputFilename: 'Stop Streaming.png'
24+
}
25+
];
26+
27+
const CANVAS_WIDTH = 1920;
28+
const CANVAS_HEIGHT = 1080;
1129

1230
function printUsageAndExit() {
1331
console.error('Missing parameter: event markdown file path is required.');
@@ -59,25 +77,25 @@ function escapeHtml(value) {
5977
.replace(/'/g, '&#039;');
6078
}
6179

62-
function buildHtml({ meetupLine, title, speakerLine, templateBase64 }) {
80+
function buildRecordingHtml({ meetupLine, title, speakerLine, templateBase64 }) {
6381
return `<!DOCTYPE html>
6482
<html>
6583
<head>
6684
<meta charset="UTF-8" />
6785
<style>
6886
html, body {
6987
margin: 0;
70-
width: 1920px;
71-
height: 1080px;
88+
width: ${CANVAS_WIDTH}px;
89+
height: ${CANVAS_HEIGHT}px;
7290
overflow: hidden;
7391
font-family: Consolas, "Liberation Mono", Menlo, Monaco, monospace;
7492
color: #6E2B7E;
7593
}
7694
7795
.canvas {
7896
position: relative;
79-
width: 1920px;
80-
height: 1080px;
97+
width: ${CANVAS_WIDTH}px;
98+
height: ${CANVAS_HEIGHT}px;
8199
background-image: url('data:image/png;base64,${templateBase64}');
82100
background-size: cover;
83101
background-position: center;
@@ -133,6 +151,79 @@ function buildHtml({ meetupLine, title, speakerLine, templateBase64 }) {
133151
</html>`;
134152
}
135153

154+
function buildStreamingHtml({ title, speakerLine, templateBase64 }) {
155+
return `<!DOCTYPE html>
156+
<html>
157+
<head>
158+
<meta charset="UTF-8" />
159+
<style>
160+
html, body {
161+
margin: 0;
162+
width: ${CANVAS_WIDTH}px;
163+
height: ${CANVAS_HEIGHT}px;
164+
overflow: hidden;
165+
font-family: Consolas, "Liberation Mono", Menlo, Monaco, monospace;
166+
color: #6E2B7E;
167+
}
168+
169+
.canvas {
170+
position: relative;
171+
width: ${CANVAS_WIDTH}px;
172+
height: ${CANVAS_HEIGHT}px;
173+
background-image: url('data:image/png;base64,${templateBase64}');
174+
background-size: cover;
175+
background-position: center;
176+
background-repeat: no-repeat;
177+
}
178+
179+
.content {
180+
position: absolute;
181+
left: 140px;
182+
right: 140px;
183+
top: 330px;
184+
text-align: center;
185+
display: flex;
186+
flex-direction: column;
187+
gap: 32px;
188+
align-items: center;
189+
}
190+
191+
.title {
192+
font-size: 88px;
193+
line-height: 1.1;
194+
font-weight: 700;
195+
max-width: 1560px;
196+
word-break: break-word;
197+
}
198+
199+
.by {
200+
font-size: 44px;
201+
line-height: 1.16;
202+
font-weight: 700;
203+
max-width: 1560px;
204+
word-break: break-word;
205+
}
206+
</style>
207+
</head>
208+
<body>
209+
<div class="canvas">
210+
<div class="content">
211+
<div class="title">${escapeHtml(title)}</div>
212+
<div class="by">by ${escapeHtml(speakerLine)}</div>
213+
</div>
214+
</div>
215+
</body>
216+
</html>`;
217+
}
218+
219+
function buildHtml(profileName, data) {
220+
if (profileName === 'recording') {
221+
return buildRecordingHtml(data);
222+
}
223+
224+
return buildStreamingHtml(data);
225+
}
226+
136227
async function main() {
137228
const eventFileArg = process.argv[2];
138229
if (!eventFileArg) {
@@ -141,7 +232,9 @@ async function main() {
141232

142233
const eventFilePath = path.resolve(ROOT_DIR, eventFileArg);
143234
assertFileExists(eventFilePath, 'Event file');
144-
assertFileExists(TEMPLATE_PATH, 'Background template');
235+
IMAGE_PROFILES.forEach(profile => {
236+
assertFileExists(profile.templatePath, `${profile.outputFilename} template`);
237+
});
145238

146239
const raw = fs.readFileSync(eventFilePath, 'utf8');
147240
const parsed = matter(raw);
@@ -160,36 +253,42 @@ async function main() {
160253

161254
const meetupLine = formatMeetupLine(date);
162255
const speakerLine = normalizeSpeakers(speakers);
163-
const templateBase64 = fs.readFileSync(TEMPLATE_PATH).toString('base64');
256+
const eventDir = path.dirname(eventFilePath);
164257

165258
const browser = await chromium.launch();
166259

167260
try {
168261
const page = await browser.newPage({
169-
viewport: { width: 1920, height: 1080 },
262+
viewport: { width: CANVAS_WIDTH, height: CANVAS_HEIGHT },
170263
deviceScaleFactor: 1
171264
});
172265

173-
const html = buildHtml({
174-
meetupLine,
175-
title: String(title).trim(),
176-
speakerLine,
177-
templateBase64
178-
});
266+
const titleText = String(title).trim();
267+
268+
for (const profile of IMAGE_PROFILES) {
269+
const templateBase64 = fs.readFileSync(profile.templatePath).toString('base64');
270+
const html = buildHtml(profile.name, {
271+
meetupLine,
272+
title: titleText,
273+
speakerLine,
274+
templateBase64
275+
});
179276

180-
await page.setContent(html, { waitUntil: 'networkidle' });
277+
await page.setContent(html, { waitUntil: 'networkidle' });
181278

182-
const outputPath = path.join(path.dirname(eventFilePath), OUTPUT_FILENAME);
183-
await page.screenshot({ path: outputPath, type: 'png' });
279+
const outputPath = path.join(eventDir, profile.outputFilename);
280+
await page.screenshot({ path: outputPath, type: 'png' });
281+
282+
console.log(`Generated: ${outputPath}`);
283+
}
184284

185-
console.log(`Generated: ${outputPath}`);
186285
} finally {
187286
await browser.close();
188287
}
189288
}
190289

191290
main().catch(error => {
192-
console.error('Error while generating recording image:');
291+
console.error('Error while generating streaming images:');
193292
console.error(error instanceof Error ? error.message : String(error));
194293
process.exit(1);
195294
});

templates/Start Streaming.png

51.6 KB
Loading

templates/Stop Streaming.png

69 KB
Loading

0 commit comments

Comments
 (0)