用AI做一个离线提词器:暂停后不跳行,改完再验收

这次做什么:做一个能在电脑浏览器本地打开的小提词器。把稿件粘进去,调字号和滚动速度,读到一半可以暂停,继续时不突然跳过几行。AI负责把这些行为写成代码;你负责用一份编号练习稿检查它是否真的做对。

准备:一个当地可用、获准使用的文字AI,一个纯文本编辑器和桌面浏览器。可从DeepSeek官方入口进入网页版,也可用你已有的文字助手。本例不需要API、服务器或软件包。官方客户端页面写有免费体验,但账号可用性、额度和地区以实际页面为准,不能据此推断API免费或无限使用。

先只把本文虚构稿与功能要求交给云端AI。其隐私政策说明服务会处理输入内容;工具做好以后,真实稿件可以只粘贴到本地文件里。生成代码时的在线对话,和运行本地提词器,是两个不同步骤。

一、把“提词器”说成可检查的行为

只说“给我一个好用的提词器”,AI可能顺手加上录音、语音跟随、云端保存或第三方字体。第一版只保留五件事:载入文字、开始与暂停、回到开头、调速度、调字号。速度使用像素/秒;它描述画面移动距离,不能直接换算成你的字数或说话时长。

这版还有三个容易漏掉的细节。暂停后继续,要从当前位置重新计时;切到其他标签页,应保持暂停等你回来;修改字号后行会重新排,先停下来找回位置,再继续。浏览器的动画回调文档说明要根据时间戳计算推进量,不能简单地“每帧挪一格”。

二、用12行虚构稿,让AI交出完整文件

下面的编号是验收标记。第10行故意包含尖括号,它应像普通文字一样出现,不能变成加粗效果。根据textContent文档,稿件可作为纯文本放入页面,避免被当成HTML解析。

01 今天只演示桌面整理。
02 先把空盒摆在左边。
03 把铅笔放进盒子。
04 留一支笔在桌上。
05 停一下,检查有没有遗漏。
06 把笔记本合上。
07 将笔记本放在盒子右边。
08 两件物品之间留出空隙。
09 这行用来检查换行是否保留。
10 <b>这应当显示为普通文字</b>
11 读完这一句,准备结束。
12 演示结束。

新建普通文字对话,把以下请求和上方练习稿一起发给AI。不必连接网盘,也不需要给予读取电脑文件的权限。

请写一个单文件离线中文提词器,完整HTML、CSS、JavaScript放在同一文件。给完整代码,不省略段落。
稿件来自textarea;点“载入稿件”后用textContent原样放入显示区,保留换行。不要用innerHTML解析稿件。
按钮只有载入、开始/暂停、回到开头;速度10—100像素/秒,字号24—72像素。
用requestAnimationFrame的时间差推进scrollTop;同一时刻只有一个动画循环。
暂停取消待执行帧;继续的第一帧重新计时,不补算暂停时长。单帧时间差最多0.1秒;到末尾停止。
显示区底部留出阅读最后一行的空间,但判断短稿是否全部可见时,不能把这段尾部留白算成正文。
修改字号、切到后台、改变窗口大小或手动滚动时先暂停;回到页面不自动继续。
空稿不启动;短稿已全部显示时提示无需滚动;回到开头必须暂停。
不用外部库、外链字体、图片、联网、API、麦克风、摄像头、文件访问、自动下载或浏览器存储。不用eval和内联事件属性。
保留原生按钮、标签、键盘焦点样式;窄窗口的控件允许换行。
提示刷新或关闭后稿件可能丢失,要另存文字备份。
将下方虚构12行练习稿放入textarea;源码中的尖括号要正确转义,显示时仍是原来的普通文字。
最后列出待执行的验收清单,不要声称已经运行。
【粘贴上方12行练习稿】

保留第一版代码。若回答中出现“其余部分略”“这里添加你的逻辑”,要求补成完整文件;别把几段缺口代码随意拼到一起。也不要因为AI写了“离线”两个字,就认定输出没有联网行为。

三、先保存完整参考,再换成自己的版本

文末附有完整参考实现,供第一次练习和比较。它是为本文编写的教学代码,不是某个产品一次生成的实测记录。参考版本没有联网、录音、摄像头或浏览器存储调用,并为外部资源与连接加了限制。这一说明只针对这里给出的代码,不能替代对新生成版本的检查,也不涵盖浏览器扩展或电脑本身。

  1. 将完整HTML复制到纯文本编辑器,另存为UTF-8的prompter.html。不要复制代码框外的说明,也不要把代码围栏一起存进去。
  2. 确认文件名不是prompter.html.txt。Windows记事本可选择“所有文件”保存类型;macOS文本编辑先切换为纯文本。不要用Word的富文本格式保存。
  3. 第一次仍只用虚构稿。检查新AI版本是否加入了外链、网络请求、权限申请或保存功能;如果看不懂这些额外代码,先用本文完整参考,不急着运行陌生改版。
  4. 断网后用桌面浏览器打开这个本地文件,点击“载入稿件”,从默认30像素/秒、40像素字号开始。先调整到实际观看距离能读清,再少量调速。

手机文件预览器不一定会执行本地HTML,这篇先以电脑浏览器为操作终点。页面不自动保存文字,刷新或关闭可能丢掉刚粘贴的稿件;先在另一个文本文件里留副本。

四、重点验收“停住”和“接着走”

开始后先读到04行,点暂停,等一会儿再点开始。它应从原位置继续,不能一下跨到09行。切到另一个标签页,再回来,仍应保持暂停。页面可见性事件可用于这一暂停逻辑;最终表现仍要在你使用的浏览器里检查。

参考实现会给最后一行留出阅读空间。判断短稿能否一次显示时,会扣除这段尾部留白;否则只有一行文字也可能被误判为需要滚动。这里用到的scrollHeight包含内边距,所以“容器有滚动条”和“正文还没显示完”不能简单当成一回事。

  1. 文字:01—12编号与换行保留,第10行的尖括号仍能看见;不要把它渲染成标签。
  2. 暂停与继续:开始会移动,暂停会停;等待后继续不跨过多行。连续操作几轮,速度也不应因为重复启动动画而变快。
  3. 切到后台:离开标签页再返回,状态应为暂停,需手动再次开始。
  4. 重置与字号:“回到开头”应停住并回到01行;改字号先暂停,再按编号找到阅读位置。
  5. 空稿与短稿:清空输入后重新载入,开始时提示先输入稿件;换成仅“只有一行”的稿件,能完整显示时应提示无需滚动。只改输入框却没点载入,显示区仍是旧稿。
  6. 结尾:完整走到12行,最终停止。底部留白用于把最后一行送到便于阅读的位置,不能因此丢掉最后一句。
  7. 本地使用:断网能打开和操作,没有登录、下载、麦克风或摄像头权限提示。断网能用只是可用性检查,不能单独证明联网时不发数据,仍需检查代码。
  8. 真实屏幕:用Tab操作按钮和滑块,缩窄窗口检查控件是否遮挡文字;刷新前保存稿件,刷新后不要期待它自动恢复。

在每一项旁边记“通过”或具体失败步骤,不要让AI替你把整张验收表填成通过。拍摄前还要在实际距离完整试读;滚动速度不是成片时长测量。参考代码对单帧时间差设有上限,设备卡顿时可能滚得更慢,以减少突然跳跃。

五、失败时带着复现步骤回去修

打开后看到整页代码,先查文件后缀与纯文本保存方式;改稿没生效,先点载入;改字号或窗口后停住,是本例的设计行为。内容很短时不滚动也可能是正确结果。先区分这些情况,再请AI改程序。

如果确实是“暂停后继续会跳行”,就只修这一件事,保留出错版本,用下面的格式报告。修完仍重做全部八项,不能因为一个按钮好了就认为其他行为没受影响。

只修复“暂停后继续会跨过多行”这一问题。
复现:开始在02行,暂停在04行;等15秒,点继续,突然到了09行。
预期:从04行附近正常继续,不把暂停的15秒计入滚动时间。
下面是当前完整代码。请解释原因并返回完整修正版,保留其他控件、纯文本输入和不联网、不存储等限制。
不要把代码检查写成已运行;我会重新执行全部验收。
【粘贴当前代码,不粘贴真实私密稿件】

完成标准:保留完整HTML、独立的文字稿和你实际填写的验收记录;在自己的浏览器上完成载入、暂停、继续、重置、改字号和结尾检查。这个小工具只负责滚动显示,不会监听语音,也不会替你录制视频。

附:完整参考代码

下面从第一行到最后一行一起复制。本地已对参考脚本做14项模拟DOM/动画队列检查,包括暂停后继续、空稿和单行稿的尾部留白问题;这不是浏览器布局、手机、无障碍或真实录制测试。上面的八项设备验收仍需你执行。

展开完整HTML参考代码
<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'none'; form-action 'none'; base-uri 'none'">
<title>离线练习提词器</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; padding: 16px; font: 18px/1.6 system-ui,sans-serif; background: #f5f4ef; color: #182126; }
main { max-width: 960px; margin: auto; }
h1 { font-size: 26px; margin: 0 0 8px; }
textarea { width: 100%; min-height: 150px; padding: 10px; font: inherit; }
.controls { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin: 14px 0; }
button { padding: 10px 16px; min-height: 44px; font: inherit; }
label { display: inline-flex; gap: 8px; align-items: center; flex-wrap: wrap; }
#viewport { height: 60vh; overflow: auto; overscroll-behavior: none; background: #14232a; color: #fff; border: 2px solid #14232a; border-radius: 12px; }
#display { white-space: pre-wrap; overflow-wrap: anywhere; font: 40px/1.7 system-ui,sans-serif; margin: 0; padding: 20px 24px 50vh; }
:focus-visible { outline: 3px solid #b34600; outline-offset: 3px; }
</style>
<main>
<h1>离线练习提词器</h1>
<p>本工具不保存稿件;刷新或关闭可能丢失。先另存一份文字稿。</p>
<label for="source">练习稿</label>
<textarea id="source" spellcheck="false">01 今天只演示桌面整理。
02 先把空盒摆在左边。
03 把铅笔放进盒子。
04 留一支笔在桌上。
05 停一下,检查有没有遗漏。
06 把笔记本合上。
07 将笔记本放在盒子右边。
08 两件物品之间留出空隙。
09 这行用来检查换行是否保留。
10 &lt;b&gt;这应当显示为普通文字&lt;/b&gt;
11 读完这一句,准备结束。
12 演示结束。</textarea>
<div class="controls">
<button id="apply" type="button">载入稿件</button>
<button id="toggle" type="button">开始</button>
<button id="reset" type="button">回到开头</button>
</div>
<div class="controls">
<label>速度 <input id="speed" type="range" min="10" max="100" value="30"> <output id="speedValue">30</output> 像素/秒</label>
<label>字号 <input id="size" type="range" min="24" max="72" value="40"> <output id="sizeValue">40</output> 像素</label>
</div>
<p id="status" role="status">已就绪</p>
<section id="viewport" tabindex="0" aria-label="提词区域">
<pre id="display"></pre>
</section>
</main>
<script>
'use strict';
const byId = id => document.getElementById(id);
const source = byId('source'), display = byId('display');
const viewport = byId('viewport'), toggle = byId('toggle');
const speed = byId('speed'), size = byId('size'), status = byId('status');
let running = false, frameId = null, previous = null, position = 0;
function stop(message = '已暂停') {
  running = false;
  if (frameId !== null) cancelAnimationFrame(frameId);
  frameId = null;
  previous = null;
  toggle.textContent = '开始';
  status.textContent = message;
}
function tick(now) {
  if (!running) return;
  if (previous !== null) {
    const seconds = Math.min((now - previous) / 1000, 0.1);
    const end = Math.max(0, viewport.scrollHeight - viewport.clientHeight);
    position = Math.min(end, position + Number(speed.value) * seconds);
    viewport.scrollTop = position;
    if (position >= end) { stop('已到结尾'); return; }
  }
  previous = now;
  frameId = requestAnimationFrame(tick);
}
function apply() {
  stop('已载入;点击开始');
  display.textContent = source.value;
  viewport.scrollTop = 0;
}
byId('apply').addEventListener('click', apply);
toggle.addEventListener('click', () => {
  if (running) { stop(); return; }
  if (!display.textContent.trim()) { stop('先输入并载入稿件'); return; }
  const tail = parseFloat(getComputedStyle(display).paddingBottom) || 0;
  if (display.scrollHeight - tail <= viewport.clientHeight) {
    stop('内容已全部显示,无需滚动'); return;
  }
  const end = Math.max(0, viewport.scrollHeight - viewport.clientHeight);
  if (end <= 1) { stop('内容已全部显示,无需滚动'); return; }
  if (viewport.scrollTop >= end - 1) viewport.scrollTop = 0;
  position = viewport.scrollTop;
  previous = null;
  running = true;
  toggle.textContent = '暂停';
  status.textContent = '正在滚动';
  frameId = requestAnimationFrame(tick);
});
byId('reset').addEventListener('click', () => { stop('已回到开头'); viewport.scrollTop = 0; });
speed.addEventListener('input', () => { byId('speedValue').textContent = speed.value; });
size.addEventListener('input', () => {
  stop('字号已改变;请找回阅读位置,再点开始');
  display.style.fontSize = size.value + 'px';
  byId('sizeValue').textContent = size.value;
});
['wheel', 'touchstart', 'pointerdown', 'keydown'].forEach(type => {
  viewport.addEventListener(type, () => { if (running) stop('已暂停,可手动找回位置'); }, { passive: true });
});
document.addEventListener('visibilitychange', () => { if (document.hidden) stop('切换页面后已暂停'); });
window.addEventListener('resize', () => { if (running) stop('窗口改变后已暂停'); });
apply();
</script>
</html>

官方资料核查于2026年10月20日。练习稿、提示词、参考代码和预期行为均为原创教学构造;未操作文字AI产品生成该文件,未进行浏览器GUI或手机实测。

© 版权声明

相关文章