Repos / cinelume / script-within OWN

script-within

剧本里 ScriptWithin

剧本里 ScriptWithin

一座可以走进去的剧场。门楣上只写一句话:请静心观看。

IMDb Top 500 电影的剧本,配上一个为「读」而生的页面。后台备着笔墨纸砚,但绝不主动递到观众手中。

产品哲学、交互铁律与语言风格见 PROMPTS.md。
阅读是呼吸,生成是呼气。用户首先且始终是读者。


快速开始

npm install          # 安装依赖
npm run fetch        # 数据管线:IMDb Top 500 + IMSDb / DailyScript / ScriptSlug 剧本(约 30 分钟,可断点续传)
npm run parse        # 预热解析缓存 → data/parsed/(阅读页零现解析;幂等,可随时补跑)
npm run dev          # 打开 http://localhost:3000

数据未备好时首页不会崩——它只递出一行指引和上面这条命令。
当前上架:500 部片单,其中 250 份完整剧本(IMSDb 156 · ScriptSlug 85 · DailyScript 9,
逐份质检:体量、场景行、残留、扫描稿、名作角色名抽查)。
未上架的影片在书架上置灰显示「暂未上架」,不做任何引导。

数据管线

python3 scripts/fetch_scripts.py fetch-imdb      # IMDb 官方数据集 → data/imdb_top500.json
python3 scripts/fetch_scripts.py fetch-index     # IMSDb 全量索引   → data/imsdb_index.json
python3 scripts/fetch_scripts.py fetch-sources   # DailyScript + ScriptSlug 索引 → data/sources_index.json
python3 scripts/fetch_scripts.py download        # 依次匹配并下载正文 → data/movies.json + data/scripts/*.txt
python3 scripts/fetch_scripts.py all             # 四步连跑
  • 榜单复现:用 IMDb 官方数据集(datasets.imdbws.com)按官方贝叶斯加权公式
    WR = v/(v+m)·R + m/(v+m)·C(m=25000)排序取前 500,与 IMDb Top 250 榜头完全一致。
    IMDb 网页端有反爬(HTTP 202),数据集是唯一稳妥来源。
    同时从 title.crew + name.basics 解析导演与编剧,供阅读页扉页展示(约 375MB 额外一次性下载)。
  • 三个剧本源,可信度递减:IMSDb(标题归一 + 模糊匹配)→ DailyScript
    (IMDb ID 精确匹配优先,标题须带年份校验)→ ScriptSlug(归一 slug + 年份 ±2)。
    年份不符的重名一律放弃——1957 版《十二怒汉》绝不撞上 1997 翻拍。
  • PDF 稿:ScriptSlug 与 DailyScript 部分稿件为 PDF,需系统装有
    pdftotext(brew install poppler)保留版式转换;没有工具或扫描版无文字层时安静跳过。
  • IMSDb 适配:2024 改版后索引在 /alphabetical/<字母>,信息页在
    /Movie Scripts/<片名> Script.html,正文在 /scripts/<Slug>.html 的 <pre> 里。
  • 礼貌爬取:请求间隔 1.5–3s 随机、host 白名单(五个域名)、失败重试两次后跳过、
    已下载文件自动跳过(断点续传)。
  • 解析质检:node --experimental-strip-types scripts/try_parser.ts
    可对全部样本跑一遍剧本解析器,输出场景数与块类型分布。

目录

scripts/fetch_scripts.py   数据管线(纯标准库,零依赖;PDF 稿借助系统 pdftotext)
scripts/parse-cache.ts     解析缓存预热(npm run parse → data/parsed/)
scripts/try_parser.ts      解析器质检脚本
lib/screenplay.ts          剧本纯文本 → 场景/对白/动作/括注/转场 结构化块
lib/script-search.ts       台词检索纯函数核(台词优先、一幕一答)
lib/local.ts               批注/收藏/草稿/幕摘要的本地存储(不出本机)+ 备份合并
lib/data.ts                数据访问层(movies.json + 剧本文件 + 解析缓存回退)
app/page.tsx               藏书首页(书架、检索、台词检索、静默收藏、备份;缺数据时递出指引)
app/movie/[slug]/page.tsx  阅读页(读/停/想/写 全部入口)
app/api/whisper/route.ts   幕后笔:LLM 代理(本机护栏 + 无钥时安静降级)
app/api/search/route.ts    台词检索接口(结构稿常驻内存,纯本地翻找)
scripts/gen-shell-art.ts   品牌外壳生成(favicon / og:image,产物已入库)
components/WhisperPanel.tsx 想:情境提问 + 走过的幕
components/QuietPanel.tsx  右侧滑入的安静面板外壳
PROMPTS.md                 Vibe Coding 提示词全集 + 项目宪法
data/                      管线产物(不入库,含版权文本)

已知留白:宽屏才有「在此处续写」的右缘入口(窄屏阅读不受打扰);
粉丝听打稿(如 LOTR 双塔)本就没有场景行,时间轴为空属正常。

阅读页的几个安静细节

  • 色彩:默认昼(亮暖纸面的日场阅览室),右上角「关灯」一键入暗夜
    (炭黑幕布的深夜剧场),选择记住、首帧不闪白。两套主题正文对比度均 ≥4.5:1;
  • 进入时一行「请静心观看」,淡入淡出,不阻塞滚动;
  • 顶部 1px 暗金进度线;宽屏右侧是全部场景的时间轴,当前场景以金线标记,
    角落有「已读 X/Y」的静默读数,不闪烁;
  • ←/→(或 j/k)在幕间翻页:面板在场时键归面板,输入态不劫持;
  • 读到哪记住哪:再次进入时右下角一行小字「上次你读到这里 · 回到那一幕」,滚开即自动消失;
  • 没有弹窗、没有推荐、没有引导浮层。

读 · 停 · 想 · 写

按产品宪法的优先级逐层实现,每一层都可以不用:

  • 读:剧本排版还原(场景/对白/括注/转场)、场景时间轴(含「已读 X/Y」幕进度)、
    场景深链(地址栏始终指向正在读的那一幕,/movie/slug#scene-14 随手可复制)、
    听(顶栏「听」把当前幕对白读给你听,浏览器语音零依赖,再点即停);
    扉页标出藏本出处(IMSDb / ScriptSlug / DailyScript);
    点正文里任何角色名,看他的全部台词并跳回原幕;
    「角色」面板是出场地图——每个角色的台词占比细线与首现幕。
  • 停:划选任何一句,点浮出的小铅笔即成批注;批注以暗金点线留在正文里,
    顶栏「批注」可回看、可跳回、可撕掉,也可导出 Markdown(连同留下的字)。
    书脊卡/场次行右上角的圆点是静默收藏,点亮无声。
    全部存本机 localStorage,阅读行为不出这台机器;
    首页「字随人走 · 备份」可把批注、字稿、收藏与续读收进一份 JSON 带走,
    读回时只合并、不覆盖(lib/local.ts 的合并逻辑有单元测试押着)。
  • 想:顶栏「想」锚定你正读到的这一幕,四个问题——这一幕发生了什么 /
    此刻某角色心里在想什么 / 这一幕在结构里承担什么 / 如果……;
    下方「走过的幕」把读过的幕各收进一句话(剧透封锁:没读到的幕不出现,只留一个计数)。
    只依据读过的文本回答,不越过眼前这一页。
  • 写:光标停在任意一行,右缘浮现 12px 的「在此处续写」。三个入口:
    续一句、试着说、在这个瞬间。生成短于已读文本的 1/5,可留(住在页边)可撕(无痕)。
    Esc 或点幕布即回到纯阅读态,无确认、无挽留。

「想」与「写」需要一支笔(OpenAI 兼容协议):

cp .env.example .env.local    # 填入 LLM_API_KEY,可选 LLM_API_BASE / LLM_MODEL / WHISPER_TOKEN

不配置也一切照常——只是「想」「写」会安静地说:后台还没有墨。
/api/whisper 有三重本机护栏:只应答本地来源(本机回环 + 局域网私网 + *.local,
公网一律降级)、可选 WHISPER_TOKEN 请求头校验、每分钟 20 次的内存令牌桶限流。
被拦下的请求与无墨时同词:不报错、不暴露。

边界

剧本文本来自公开网络,仅供个人学习研究,请勿商用或公开部署。
不使用阅读行为做任何推荐。数据目录已整体 gitignore,不会随仓库分发。


项目评估(2026.09)

技术栈

  • 前端:Next.js 15 (App Router) + React 19 + TypeScript + Tailwind CSS 4,零额外运行时依赖
  • 数据:本地 JSON + 文本文件,无数据库(服务端 readFileSync + React cache)
  • 管线:纯 Python 标准库爬虫,零第三方依赖
  • AI:OpenAI 兼容协议代理(/api/whisper),无密钥时安静降级

功能点清单

数据管线(scripts/fetch_scripts.py)

  • IMDb Top 500 榜单复现:官方数据集 + 贝叶斯加权公式 WR = v/(v+m)·R + m/(v+m)·C(m=25000)
  • 三个剧本源匹配,可信度递减:IMSDb(标题归一 + 模糊匹配)→ ScriptSlug(归一 slug +
    年份校验)→ DailyScript(IMDb ID 精确 + 年份校验);年份不符的重名一律放弃
  • 质量防线:正文 >2000 字符门槛、扫描稿重复行检测、120s 涓流读取时限、单源失败自动换源
  • 礼貌爬取:1.5–3s 随机限速、HTTPS + host 白名单(五域名)、直连不走系统代理、
    重试后跳过、断点续传、--limit 试跑
  • 正文清洗(剥 HTML/防框架 JS/cp1252 控制符)+ 解析器质检脚本

剧本解析器(lib/screenplay.ts)

  • 纯文本 → 场景 / 动作 / 角色 / 对白 / 括注 / 转场六类结构化块
  • 「内容模式 + 相对缩进」规则,动态动作基线,适配 IMSDb 各稿排版差异
  • 场景行编号噪声清理、动作段与对白段合并、扉页识别

藏书首页(Library.tsx)

  • 500 部书脊卡:可读点亮、未上架置灰且无引导 CTA
  • 客户端检索 / 题材筛选 / 只看已上架 / 只看收藏 / 排名·年份·评分·片名四种排序
  • 台词检索:输入一句台词,全库答出「哪部、哪一幕」——/api/search 把
    250 份结构稿常驻内存,纯本地翻找;结果行复用场次行语言,直达深链
  • 静默收藏圆点;首访开场白(localStorage 记忆后淡出)
  • 「字随人走 · 备份」:批注/字稿/收藏/续读的导出与合并读入

阅读页(Reader.tsx,项目灵魂)

  • 读:Courier 剧本排版还原、顶部 1px 暗金进度线、宽屏场景时间轴(IntersectionObserver 追踪当前幕)、续读记忆、角色对照(点角色名看全部台词并跳回原幕)
  • 停:划选 → 铅笔 → 批注(锚定块下标 + 引文暗金点线高亮),面板回看 / 跳回 / 撕掉
  • 想:锚定当前幕三问——发生了什么 / 某角色心理(可换人)/「如果……」,上下文限当前幕、不剧透
  • 写:宽屏右缘「在此处续写」三入口(续一句 / 试着说 / 在这个瞬间),输出上限 ≈ 已读 1/5(120–480 字符钳制),可留(页边金线引用块)可撕(无痕)
  • Esc 逐层退场(稿纸 → 面板 → 铅笔),无弹窗无挽留,滚动即收起铅笔

AI 代理(app/api/whisper/route.ts)

  • OpenAI 兼容协议(默认 z.ai / glm-4.6),六种模式各自提示词(字数上限、语气、不剧透约束)
  • 无密钥返回 noInk 安静降级,所有错误化为一句文案,45s 超时

本地存储(lib/local.ts)

  • 批注 / 草稿 / 收藏 / 阅读位置全部 localStorage,阅读行为不出本机,读失败静默回退

评价

优点

  • 产品定位独特且贯彻彻底:五条交互铁律在代码里真实落地(无 toast、无确认、无挽留、Esc 分层退场、降级文案化)
  • 代码极干净:约 2200 行完成完整产品,零冗余依赖,注释只写「为什么」
  • 工程细节扎实:爬虫白名单/限速/续传、解析器相对缩进策略、maxChars 双端钳制一致

不足 / 风险(2026.09.17 复核后余量)

  • ~~解析器无单元测试~~ ✅ 已落地:17 用例 + 两项全库缺陷修复(裸页码块 3739→0、括注误判 5681→97)
  • ~~阅读页无构建期缓存~~ ✅ 已落地:npm run parse 预热 data/parsed/,运行时读缓存、缺失时回退现解析
  • ~~/api/whisper 无鉴权与限流~~ ✅ 已落地:localhost 来源限制 + 可选 WHISPER_TOKEN + 内存令牌桶
  • ~~数据缺失时首页崩溃~~ ✅ 已落地:lib/data.ts 安静降级,首页递出指引而非报错
  • 剧本版权归上游,只能止步个人学习研究;公开化需公版剧场或授权线(见 competitive-analysis.md)
  • 窄屏无「写」入口(刻意留白);均匀缩进转录稿的对白识别仍有固有歧义(script-formats.md 已记)