ScreenCI:把教程视频变成 E2E 测试,由你的编码智能体编写
Show HN: ScreenCI – Tutorial videos as E2E tests, written by your coding agent
ScreenCI 可将一句提示词交给编码智能体,自动录制真实应用的旁白演示视频,并生成对应的 E2E 测试文件。它支持自动缩放跟随点击、拖拽等交互,可嵌入图片与 MP4 素材、生成自动更新的文档截图,旁白覆盖 79 种语言,兼容任意智能体且免费试用、无需账号。
你应用的教程视频,来自一个提示词。
描述一个功能,把提示词粘贴到你的编码代理中。它会录制一段你真实应用的带旁白演示,可直接用于帮助中心、文档、社交媒体或产品本身。
适用于任何代理,例如
免费试用,无需账户。
真实渲染,绝非样机
返回内容
这里的每个片段都是真实应用的 ScreenCI 真实渲染,由一个提示词生成。镜头、旁白、字幕、品牌标识和语言都随每个教程一同生成。
镜头自动跟随流程
自动缩放追踪每一次点击、输入框和拖拽。
显示生成的端到端测试文件
镜头自动跟随流程
镜头会放大并平移掠过屏幕,跟随每一次交互:输入字段、分段控件、滑块拖拽、下拉选择。无需设置手动缩放点,除非你想明确指定。
import { autoZoom, hide, resetZoom, video } from 'screenci'
// Auto-zoom follows the action: the camera zooms and pans to whatever you touch,
// with no manual zoom points. The "Add a contact" form keeps its controls on two
// side-by-side cards (contact details left, plan and access right), so this one
// block zig-zags corner to corner and the camera makes big jumps between each
// interaction.
video('Auto-zoom', async ({ page }) => {
await hide(async () => {
await page.goto('https://demo.screenci.com/customers/new')
await page.waitForLoadState('networkidle')
})
// A tight `amount` frames each control closely, so the camera makes a big pan
// across the screen to follow each next interaction.
await autoZoom(
async () => {
// Top-left: type into the work email field.
await page
.getByLabel('Work email')
.pressSequentially('[email protected]')
// Jump top-right: pick a plan from the segmented control.
await page.getByRole('button', { name: 'Scale' }).click()
// Jump back left: type into the full-name field.
await page.getByLabel('Full name').pressSequentially('Emma Carter')
// Jump right: drag the seat-count slider up to ~62 of 100. dragTo targets a
// point along the slider root, and the camera follows the thumb across it.
const thumb = page.locator('[data-slot="slider-thumb"]').first()
const track = thumb.locator('xpath=ancestor::*[@data-slot="slider"][1]')
const trackBox = (await track.boundingBox())!
await thumb.dragTo(track, {
targetPosition: {
x: (trackBox.width * (62 - 1)) / (100 - 1),
y: trackBox.height / 2,
},
moveDuration: 900,
dragDuration: 1000,
dragSteps: 32,
})
// Bottom corners: open a dropdown on each side and pick.
await page.getByRole('combobox', { name: 'Account type' }).click()
await page.getByRole('option', { name: 'Customer' }).click()
await page.getByRole('combobox', { name: 'Region' }).click()
await page.getByRole('option', { name: 'Asia Pacific' }).click()
},
{ amount: 0.45 }
)
await resetZoom()
})插入图片和视频
你自己的素材,按时间轴定时。
显示生成的端到端测试文件
插入图片和视频
将 PNG/SVG 图片和 MP4 片段作为定时文件叠加层放置。这与程序化叠加层不同:媒体是真实的素材文件,而非 React 或 HTML 叠加层。
import { hide, video } from 'screenci'
// Embed media files directly: images/SVGs and MP4 clips are assets on the
// timeline. Programmatic overlays are React/HTML components; these are files.
video.overlays({
logo: { path: './assets/logo.png', fill: 'recording', duration: 2100 },
preview: {
path: './assets/1-sample-code.mp4',
fill: 'recording',
volume: 0,
time: 3200,
},
})('Embed media', async ({ page, overlays }) => {
await hide(async () => {
await page.goto('https://demo.screenci.com/analytics')
await page.waitForLoadState('networkidle')
})
await overlays.logo()
await page.waitForTimeout(900)
await overlays.preview()
await page.waitForTimeout(1400)
})自动更新的文档截图
来自同一脚本的带边框静帧。
显示生成的端到端测试文件
自动更新的文档截图
用 screenshot() 将任意页面捕获为带边框的图像,配上代码叠加层,并在每次重新渲染时自动更新。
import { screenshot } from 'screenci'
const BACKGROUND_CSS =
'radial-gradient(circle at top left, #dbeafe 0%, #eff6ff 28%, #f8fafc 58%, #e0f2fe 100%)'
// A framed still of a live page, dressed with two code-built overlays (a status
// pill and a lower-third caption). Because it comes from the same script as the
// videos, it re-renders and stays current on every run, no manual re-capture.
const shot = screenshot.overlays({
// A React lower-third with props.
caption: {
path: './overlays/Callout.tsx',
props: { title: 'Framed, branded, always current' },
x: 300,
y: 640,
width: 640,
},
// A plain HTML status pill.
statusPill: { path: './overlays/status-pill.html', x: 1000, y: 700, width: 420 },
})
screenshot.use({
renderOptions: {
screenshot: { aspectRatio: '16:9', margin: 96, mouse: { show: false } },
recording: {
roundness: 0.05,
dropShadow: 'drop-shadow(0 22px 49px rgba(0,0,0,0.28))',
},
output: {
background: { backgroundCss: BACKGROUND_CSS },
},
},
})
shot('Branded screenshot', async ({ page, overlays }) => {
await page.goto('https://demo.screenci.com/sales/d3')
await page.getByRole('heading', { name: 'Annual renewal and expansion' }).waitFor()
// A still has no timeline: start each overlay and leave it open so it stays
// baked into the captured image.
await overlays.caption.start()
await overlays.statusPill.start()
})79 种语言旁白
这里的每一行都是不同的语言。
显示生成的端到端测试文件
79 种语言旁白
一次录制,通过逐条提示的语言覆盖,让每一行旁白用不同的语言说出。AI 语音和字幕,全部来自同一脚本。
import { hide, video } from 'screenci'
// One recording, every language: each cue overrides its own `language`, so a
// single render greets you in a different tongue on every line.
video.narration({
hello: { cue: 'Hello from ScreenCI.', language: 'en' },
spanish: { cue: '¡Compatible con el español!', language: 'es' },
french: { cue: 'Compatible avec le français !', language: 'fr' },
german: { cue: 'Unterstützt Deutsch!', language: 'de' },
chinese: { cue: '支持中文!', language: 'cmn' },
japanese: { cue: '日本語に対応しています!', language: 'ja' },
hindi: { cue: 'हिंदी भी चलती है!', language: 'hi' },
})('Multilanguage narration', async ({ page, narration }) => {
await hide(async () => {
await page.goto('https://demo.screenci.com/')
await page.waitForLoadState('networkidle')
})
await narration.hello()
await narration.spanish()
await narration.french()
await narration.german()
await narration.chinese()
await narration.japanese()
await narration.hindi()
})每个市场都有自己的版本
一次录制,为每种语言本地化。
显示生成的端到端测试文件
每个市场都有自己的版本
录制一次页面,然后以你列出的每种语言渲染,每种都有自己的 AI 旁白和字幕。更新源文件,所有语言会同步重新渲染。
import { hide, video } from 'screenci'
// Localize the videos: record the /welcome page once, and ScreenCI renders it in
// every language you list, each with its own narration. `browserLocale` (on by
// default) sets the browser locale per render, so the page localizes itself and
// the narration matches.
// Fill the whole output frame (no gradient margin) so the localized content is
// full-bleed, and stays that way when embedded by the compose showcase. Keeps
// the shared marketing background + framing, but overrides size to 1 (full
// bleed) so this clip has no gradient margin.
video
.renderOptions({
output: {
background: {
backgroundCss:
'radial-gradient(circle at top left, #dbeafe 0%, #eff6ff 28%, #f8fafc 58%, #e0f2fe 100%)',
},
},
recording: { size: 1, roundness: 0.04, dropShadow: 1 },
})
// The target: the localized /welcome page, recorded in en/es/fi.
.narration({
en: { intro: 'Welcome to Northstar. Let’s set up your workspace.' },
es: { intro: 'Bienvenido a Northstar. Vamos a configurar tu espacio.' },
fi: { intro: 'Tervetuloa Northstariin. Otetaan työtila käyttöön.' },
})('Localized welcome', async ({ page, narration }) => {
await hide(async () => {
await page.goto('https://demo.screenci.com/welcome')
await page.waitForLoadState('networkidle')
})
await narration.intro()
await page.waitForTimeout(800)
})客户数据绝不进入录像
机密信息在画面捕获前已被遮蔽。
显示生成的端到端测试文件
客户数据绝不进入录像
用 redact() 遮蔽 API 密钥、令牌和个人身份信息。像素在画面捕获前已在页面中被遮蔽,因此机密信息绝不会进入上传的录像。这不是表面叠加:是真正的遮蔽。
import { hide, resetZoom, video, zoomTo } from 'screenci'
// True redaction: the mask is applied in the browser before any frame is
// captured, so the secret never enters the uploaded recording.
video('Redact secrets', async ({ page }) => {
const visibleSecret = 'sk_live_51ExistingValue8f1c2a'
const redactedSecret = 'sk_live_51P0J8vX2f7r9d3cH'
await hide(async () => {
await page.goto('https://demo.screenci.com/settings/company')
await page.waitForLoadState('networkidle')
await page.getByLabel('Tax ID').fill(visibleSecret)
})
const taxId = page.getByLabel('Tax ID')
await zoomTo(taxId, { amount: 0.5, duration: 300 })
await page.waitForTimeout(700)
await taxId.fill(redactedSecret, {
duration: 900,
redact: true,
})
await resetZoom()
})默认符合品牌风格
将每个片段框在你的品牌背景上。
显示生成的端到端测试文件
默认符合品牌风格
在网页编辑器中选择渲染背景,将每个录像框在带圆角和柔和阴影的品牌渐变背景上。
import { hide, video } from 'screenci'
// Frame every recording on your own brand background, with size, corner
// roundness, and shadow set once in code (or with the same controls in the
// editor).
video
.renderOptions({
output: {
background: {
backgroundCss:
'linear-gradient(135deg, oklch(0.62 0.17 268), oklch(0.4 0.18 275))',
},
},
recording: { size: 0.82, roundness: 0.06, dropShadow: 1 },
})
.overlays({
caption: { path: './overlays/Callout.tsx', x: 72, y: 760, width: 640 },
})('Styled backgrounds', async ({ page, overlays }) => {
await hide(() => page.goto('https://demo.screenci.com/'))
await overlays.caption.start()
await page.getByRole('button', { name: 'Pipeline', exact: true }).click()
await page.locator('[data-deal-card]', { hasText: 'Vega Logistics' }).click()
await overlays.caption.end()
})无聊的步骤快速略过
速度渐变跳过重复部分。
显示生成的端到端测试文件
无聊的步骤快速略过
用 speed() 快进重复的步骤,让每个演示都保持紧凑。
import { hide, resetZoom, speed, time, video, zoomTo } from 'screenci'
// Speed ramps two ways. Explicit `speed` or `time`, both work:
// speed(n): a RELATIVE multiplier on playback. n > 1 fast-forwards, n < 1 slows
// down. Best for compressing (or stretching) repetitive stretches.
// time(ms): an ABSOLUTE target. The block is retimed to occupy exactly this
// long, however long it really took, so you can pin one beat to a
// precise duration.
// Shared marketing render styling (project-wide look): light radial-gradient
// background, framed recording tile with soft corners and a drop shadow.
video.renderOptions({
output: {
background: {
backgroundCss:
'radial-gradient(circle at top left, #dbeafe 0%, #eff6ff 28%, #f8fafc 58%, #e0f2fe 100%)',
},
},
recording: { size: 0.9, roundness: 0.04, dropShadow: 1 },
})('Speed ramps', async ({ page }) => {
await hide(async () => {
await page.goto('https://demo.screenci.com/settings/company')
await page.waitForLoadState('networkidle')
})
await zoomTo(page.locator('form').first())
// speed(): fast-forward the repetitive fields into a quick montage (3x).
await speed(3, async () => {
await page
.getByLabel('Legal name')
.fill('Aperture Bio Inc', { editId: 'fill1' })
await page.getByLabel('Tax ID').fill('47-2819001', { editId: 'fill11' })
await page.getByLabel('Phone').fill('(512) 555-0142', { editId: 'fill10' })
await page
.getByLabel('Address')
.fill('2200 Congress Ave', { editId: 'fill9' })
await page.getByLabel('City').fill('Austin', { editId: 'fill2' })
await page.getByLabel('Postal code').fill('78701', { editId: 'fill3' })
await page.getByLabel('Country').fill('United States', { editId: 'fill8' })
})
// time(): slow one deliberate beat down to an exact three seconds, so the last
// field types out in emphasized slow motion.
await time(3000, async () => {
await page
.getByLabel('Billing email')
.pressSequentially('[email protected]', {
delay: 40,
editId: 'pressSequentially2',
})
})
await page.waitForTimeout(500)
await resetZoom()
})看起来像拍摄的,而非脚本化的
每次缩放和移动都有自然的运动模糊。
显示生成的端到端测试文件
看起来像拍摄的,而非脚本化的
缩放和光标移动在渲染时获得自然的运动模糊。
import { autoZoom, hide, resetZoom, video } from 'screenci'
// Own intent (strong zoom + cursor motion blur) plus the shared marketing
// render styling: light radial-gradient background and framed recording tile.
video.renderOptions({
zoom: { motionBlur: 1 },
mouse: { motionBlur: 1 },
output: {
background: {
backgroundCss:
'radial-gradient(circle at top left, #dbeafe 0%, #eff6ff 28%, #f8fafc 58%, #e0f2fe 100%)',
},
},
recording: { size: 0.9, roundness: 0.04, dropShadow: 1 },
})('Motion blur', async ({ page }) => {
await hide(async () => {
await page.goto('https://demo.screenci.com/analytics')
await page.waitForLoadState('networkidle')
})
// Every cursor move goes to something real: switch analytics views, glance at
// the headline each view reveals, then pan back up to Overview. The tight zoom turns each hop into a fast pan, so both the camera
// and the cursor show motion blur on moves that read as intentional.
await autoZoom(
async () => {
const tab = (name: string) => page.getByRole('button', { name })
const stat = (label: string) =>
page.getByText(label, { exact: true }).first()
await tab('Revenue').click()
await page.waitForTimeout(400)
await stat('ARR').hover()
await page.waitForTimeout(600)
await tab('Retention').click()
await page.waitForTimeout(400)
await stat('Net retention').hover()
await page.waitForTimeout(600)
await tab('Cohorts').click()
await page.waitForTimeout(400)
await page.getByRole('heading', { level: 3 }).first().hover()
await page.waitForTimeout(600)
await tab('Overview').click()
await page.waitForTimeout(400)
},
{ amount: 0.5, duration: 450, zoomOutDuration: 350, easing: 'ease-in-out' },
)
await page.waitForTimeout(500)
await resetZoom({ zoomOutDuration: 350 })
})你的设计系统,融入视频之中
标注和圆环,锚定到你的实时界面。
显示生成的端到端测试文件
你的设计系统,融入视频之中
用你自己的 React、Vue、Svelte 或 Solid 组件,或纯 HTML 和 CSS 构建的环形、标注、徽章和字幕。锚定在实时元素旁边,使用你应用的颜色进行样式设置,并在你的 .screenci.ts 文件中设定时间。
import { hide, video } from 'screenci'
import type { Locator } from '@playwright/test'
// Programmatic overlays: rings, callouts, and captions from code, anchored to
// the live UI and timed to the action. React and plain HTML side by side,
// over the demo app's deal pipeline.
video.overlays({
// An accent ring sized to the live element.
ring: (target: Locator) => ({ path: './overlays/ring.html', over: target, margin: 6 }),
// Your React component, placed beside a live element.
deal: (target: Locator) => ({
path: './overlays/DealCallout.tsx',
props: { company: 'Northwind Labs', value: '$61,200', probability: 0.75 },
anchor: target, side: 'right',
}),
// A React lower-third and a plain-HTML chip at fixed spots.
card: { path: './overlays/Callout.tsx', x: 72, y: 760, width: 640 },
htmlChip: { path: './overlays/chip.html', x: 1440, y: 72, width: 400 },
})('Programmatic overlays', async ({ page, overlays }) => {
await hide(() => page.goto('https://demo.screenci.com/pipeline'))
const card = page
.locator('[data-stage="Negotiation"] [data-deal-card]')
.filter({ hasText: 'Northwind Labs' })
const ring = overlays.ring(card)
const deal = overlays.deal(card)
await ring.start()
await deal.start()
await page.waitForTimeout(1800)
await deal.end()
await ring.end()
await card.dragTo(page.locator('[data-stage="Closed won"]'))
await overlays.card.start()
await overlays.htmlChip.start()
await page.waitForTimeout(2600)
await overlays.htmlChip.end()
await overlays.card.end()
})竖屏适配社交,一个设置
更改宽高比,其他什么都不用动。
显示生成的 e2e 测试文件
竖屏适配社交,一个设置
将 aspectRatio 切换为 9:16,录制器会自动打开竖屏视口。无需裁剪,无需重新构图。
import { hide, video } from 'screenci'
// Vertical for social, one setting: the same demo app recorded at 9:16. The
// app's responsive layout takes over (the sidebar collapses into a top nav),
// and the render frames it for Reels, Shorts, and TikTok.
video
.renderOptions({
recording: { size: 0.96, roundness: 0.04 },
output: {
aspectRatio: '9:16',
quality: '1080p',
background: {
backgroundCss:
'radial-gradient(circle at top left, #dbeafe 0%, #eff6ff 28%, #f8fafc 58%, #e0f2fe 100%)',
},
},
})
.recordOptions({ aspectRatio: '9:16', quality: '720p' })(
'Vertical video',
async ({ page }) => {
await hide(async () => {
await page.goto('https://demo.screenci.com/')
await page.waitForLoadState('networkidle')
})
await page.waitForTimeout(1200)
await page.mouse.wheel(0, 700)
await page.waitForTimeout(1500)
await page.getByRole('button', { name: 'Inbox' }).click()
await page.waitForTimeout(1500)
await page.getByText('Onboarding timeline for the board').click()
await page.waitForTimeout(1800)
},
)告诉旁白如何发声
逐行指定语气和表达方式。
显示生成的 e2e 测试文件
告诉旁白如何发声
通过一个通俗易懂的风格提示,逐条指定语气和表达方式,从平静的向导到十足的海盗。富有表现力的语音模型可解锁更多语言。所有套餐均可用。
import { video, voices } from 'screenci'
// Each cue carries its own style prompt, so the same voice adapts its
// delivery per line. Write the direction in plain English.
video.narration({
hook: {
cue: 'Every narration line can have its own speaking style.',
voice: {
name: voices.Nora,
style: 'Warm and intriguing, drawing the viewer in.',
},
},
pirate: {
cue: 'Arr! Even a swashbuckling pirate can guide the tour, if that be the style ye prompt.',
voice: {
name: voices.Nora,
style: 'A boisterous pirate captain, gravelly and full of swagger.',
},
},
whisper: {
cue: 'Or lean right in, because the best part is a little secret.',
voice: {
name: voices.Nora,
style: 'A hushed, conspiratorial whisper, close to the mic.',
},
},
outro: {
cue: 'Write the style in plain English. The voice does the rest.',
voice: {
name: voices.Nora,
style: 'Confident and conclusive, landing the final thought.',
},
},
})('Per-cue speaking style', async ({ page, narration }) => {
await page.goto('https://demo.screenci.com/')
await narration.hook()
await narration.pirate()
await narration.whisper()
await narration.outro()
})每个视频都包含
一个完整教程所需的一切
后期无需添加任何内容。语音、镜头、字幕、语言和链接都来自同一个提示。
自然的 AI 语音同步朗读每一步,镜头跟随动作,光标像手一样移动,字幕从第一条就开启。逐行选择语音及其语气,或带上你的 ElevenLabs API 密钥,用你自己声音的克隆来旁白。
脚本就是测试,所以每次录制都完全相同。当产品发生变化时,一键即可重新录制。
文档插图和 README 图片是同一脚本的静帧,带有品牌标识、经过裁剪,并在 UI 变化时重新截取。
支持和文档团队可以改写一行、更换语音或剪掉缓慢的部分。无需向工程团队提交工单;编辑会同步回脚本。
一个源视频,以你列出的每种语言进行旁白和字幕,每种语言都在本地化页面上录制。
在你的帮助中心或文档中嵌入一次 CDN 链接。它始终提供你批准的版本。
要求一个介绍卡片或标注,代理会用你自己的组件绘制它,因此与产品风格一致。
你的源代码和浏览器会话保持原样。只有录制内容、其时间数据和视频脚本会被上传,并且 redact() 会在捕获前屏蔽敏感信息。录制器是开源的。
可选,面向工程团队
额外好处:每个教程同时也是一个端到端测试
这里没有任何强制要求:你只需在笔记本电脑上粘贴提示并在浏览器中编辑,就能获得完整产品。但每个视频背后的脚本恰好是一个普通的 Playwright 测试,所以如果你的工程师愿意,他们可以将其保留在代码仓库中,并让 CI 在每次发布时重新录制。
存放在你的代码仓库中
代理会将视频脚本提交到你的代码旁边。任何能读懂 Playwright 测试的人都可以审阅、进行版本管理并阅读它们。
每次发布时重新录制
添加到 CI 会在你自己的运行器上设置一条流水线,因此视频始终展示当前产品。
流程中断会导致构建失败
如果教程所走的流程停止工作,运行就会像任何其他测试一样失败,在过时的视频到达客户之前。
不产生额外费用
无论 CI 运行多少次,录制和预览都是免费的。你只需为导出付费。
常见问题
常见问题
其他所有内容都在文档中,而且提示本身会在执行之前解释它将要做什么。
这适用于什么样的视频?
教程:帮助中心演示、新手引导和文档截图,以及那些必须随产品变化而保持正确的视频。发布和上线短片也是同样的做法。参见通过提示词制作视频。
团队里由谁来粘贴提示词?
任何已经打开了 Claude Code 或 Codex 的人:CTO、工程师、产品负责人。这一次粘贴就完成了项目设置。之后,支持和文档人员可以在浏览器中编辑旁白和措辞,完全不需要 agent;一旦项目从 CI 录制,重新录制在应用中只需一键。
我需要会写代码吗?
不需要。你只需用一句话描述视频,编码 agent 会编写并运行其余的一切。底层上,视频就是一个脚本,但你永远不必打开它:旁白、语音、叠加层和语言都可以在网页编辑器中编辑。参见通过提示词制作视频。
旁白可以用我自己的声音吗?
可以。在浏览器中录制一段简短样本,或拖入一个音频或视频文件,ScreenCI 会通过你自己的 ElevenLabs API 密钥克隆它(在品牌页面添加一次,加密保存,之后不再显示)。克隆功能适用于所有套餐;来自你 ElevenLabs 库的语音在更高套餐中可用。参见ElevenLabs 语音。
哪些编码 agent 可以使用?
任何能运行 shell 命令的 agent 或工具链:Claude Code、Codex、Cursor、pi.dev、桌面 agent 等。提示词会告诉 agent 从哪里获取指令,一次性代码会将它连接到你的项目。agent 只会要求你在它打开的浏览器中登录你自己的产品,绝不会索要密码。
我需要会使用终端吗?
不需要。你把提示词粘贴到 agent 中,它会自行运行所有命令:安装 ScreenCI、编写脚本、录制,并在浏览器中打开结果。如果 agent 需要你提供任何信息,它会用通俗的语言询问。参见通过提示词制作视频。
我的应用或源代码会离开我的机器吗?
录制在本地针对你自己的应用运行,你应用的源代码永远不会离开你的机器。上传的是带有时间数据的屏幕录制,以及 screenci/ 文件夹的文本文件:配置和视频脚本,以便网页应用可以展示它们并交给编码 agent(设置 uploadSources: false 可让它们不被上传)。绝不会上传 env 文件、lockfile 或该文件夹之外的任何内容。发送内容的完整列表。CLI 是开源的,所以你可以自行验证。
当我的 UI 发生变化时会怎样?
点击重新录制,或让 CI 在下一次推送时执行,教程就会针对新 UI 重新渲染。如果流程本身出了问题,运行会像端到端测试一样失败,这样你就能在用户之前发现问题。参见仓库与 CI。
工程师可以把视频保留在仓库中吗?
可选。在仓库中运行提示词,agent 会在一个分支上提交脚本,添加到 CI 会设置一条流水线,在每次推送时重新录制它们。这样每个视频就变成了你的 CI 已经知道如何运行的 Playwright 端到端测试。参见仓库与 CI。
它是开源的吗?
CLI 和创作 API 在 GitHub 上以MIT 许可证发布,录制使用它们在本地运行。渲染(旁白、镜头、字幕、叠加层)、网页编辑器和托管是付费服务,并非开源。
如果 ScreenCI 消失了,我的视频会怎样?
这些脚本是你自己仓库中的 Playwright 测试,因此它们会继续作为测试运行。导出的是你下载的 MP4 文件(npx screenci export 会将它们写入 ./exports),所以你可以将它们托管在任何地方。只有托管的公开 URL 依赖于该服务。
费用是多少?
试用是免费的,且无需账号:三次带实时预览的试用录制。在所有套餐中,录制和实时预览都保持免费;只有渲染后的导出才计费,因此费用取决于你发布的视频,而不是你运行的次数。有关导出、语言和团队套餐,请查看定价。
为什么不直接使用 Playwright 内置的视频录制功能?
Playwright 的录制是原始的调试捕获。ScreenCI 将同一脚本变成成品产品视频:与每个步骤同步的 AI 旁白、自动缩放镜头、动画光标和打字效果、来自你自己 React/Vue/Svelte 组件的叠加层、字幕、翻译,以及一个永久的 CDN URL。看看脚本长什么样。
免费试用,无需账号
粘贴一条提示词。获取你的第一个教程。
三次带实时预览的试用录制,直接使用本页顶部的提示词即可。只有想保留它们时才需要注册。
适用于 Claude Code、Codex、Cursor、pi.dev,以及任何能运行 shell 命令的 agent。
要在团队中推广?预约 30 分钟通话
来源:Hacker News · screenci.com