(artifact: Artifact)
| 233 | }; |
| 234 | |
| 235 | const toGif = async (artifact: Artifact): Promise<string> => { |
| 236 | if (artifact.path.endsWith(".png")) return artifact.path; |
| 237 | const out = join(tmpdir(), `${artifact.slug}.gif`); |
| 238 | if (artifact.path.endsWith(".cast")) { |
| 239 | run("agg", ["--idle-time-limit", "2", "--font-size", "14", artifact.path, out]); |
| 240 | } else { |
| 241 | // Two-pass palette keeps UI text crisp at a size GitHub will render. |
| 242 | const duration = videoDuration(artifact.path); |
| 243 | const segments = urlSegments(artifact, duration); |
| 244 | if (segments.length > 0) { |
| 245 | const workDir = mkdtempSync(join(tmpdir(), "pr-media-urlbar-")); |
| 246 | try { |
| 247 | const bars = await renderUrlBars(segments, workDir); |
| 248 | const scaled = |
| 249 | `[0:v]fps=8,scale=${GIF_WIDTH}:-2:flags=lanczos,` + |
| 250 | `pad=iw:ih+${CHROME_HEIGHT}:0:${CHROME_HEIGHT}:color=0x111318,setsar=1[v0]`; |
| 251 | const overlays = bars.map(({ segment }, index) => { |
| 252 | const input = index + 1; |
| 253 | const previous = `v${index}`; |
| 254 | const next = `v${index + 1}`; |
| 255 | const from = segment.from.toFixed(3); |
| 256 | const to = segment.to.toFixed(3); |
| 257 | return `[${previous}][${input}:v]overlay=0:0:enable='between(t,${from},${to})'[${next}]`; |
| 258 | }); |
| 259 | const finalVideo = `v${bars.length}`; |
| 260 | const palette = |
| 261 | `[${finalVideo}]split[a][b];` + |
| 262 | "[a]palettegen=max_colors=128[p];[b][p]paletteuse=dither=bayer:bayer_scale=5[out]"; |
| 263 | run("ffmpeg", [ |
| 264 | "-y", |
| 265 | "-loglevel", |
| 266 | "error", |
| 267 | "-i", |
| 268 | artifact.path, |
| 269 | ...bars.flatMap((bar) => ["-i", bar.path]), |
| 270 | "-filter_complex", |
| 271 | [scaled, ...overlays, palette].join(";"), |
| 272 | "-map", |
| 273 | "[out]", |
| 274 | out, |
| 275 | ]); |
| 276 | } finally { |
| 277 | rmSync(workDir, { recursive: true, force: true }); |
| 278 | } |
| 279 | } else { |
| 280 | const filter = |
| 281 | `fps=8,scale=${GIF_WIDTH}:-2:flags=lanczos,split[a][b];` + |
| 282 | "[a]palettegen=max_colors=128[p];[b][p]paletteuse=dither=bayer:bayer_scale=5"; |
| 283 | run("ffmpeg", ["-y", "-loglevel", "error", "-i", artifact.path, "-vf", filter, out]); |
| 284 | } |
| 285 | } |
| 286 | return out; |
| 287 | }; |
| 288 | |
| 289 | /** Commit a file to the media branch via the git database API; returns its raw URL. */ |
| 290 | const upload = (repo: string, localPath: string, mediaPath: string): string => { |
no test coverage detected