@leiboss/dsh-voiceover 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +187 -2
- package/client/client.js +985 -0
- package/cordis.patch.yml +13 -0
- package/dist/api.d.ts +52 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +301 -0
- package/dist/api.js.map +1 -0
- package/dist/cli.d.ts +40 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +303 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.js +985 -0
- package/dist/http-types.d.ts +26 -0
- package/dist/http-types.d.ts.map +1 -0
- package/dist/http-types.js +2 -0
- package/dist/http-types.js.map +1 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +410 -0
- package/dist/index.js.map +1 -0
- package/dist/llm.d.ts +60 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +113 -0
- package/dist/llm.js.map +1 -0
- package/dist/operations.d.ts +41 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +77 -0
- package/dist/operations.js.map +1 -0
- package/dist/panel.d.ts +42 -0
- package/dist/panel.d.ts.map +1 -0
- package/dist/panel.js +212 -0
- package/dist/panel.js.map +1 -0
- package/dist/pipeline.d.ts +141 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +519 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/prompts.d.ts +58 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +169 -0
- package/dist/prompts.js.map +1 -0
- package/dist/serve-entry.d.ts +3 -0
- package/dist/serve-entry.d.ts.map +1 -0
- package/dist/serve-entry.js +11 -0
- package/dist/serve-entry.js.map +1 -0
- package/dist/snapshot.d.ts +48 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +82 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/subtitle.d.ts +49 -0
- package/dist/subtitle.d.ts.map +1 -0
- package/dist/subtitle.js +163 -0
- package/dist/subtitle.js.map +1 -0
- package/dist/types.d.ts +148 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +16 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +47 -0
- package/dist/util.js.map +1 -0
- package/dist/validate.d.ts +15 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +186 -0
- package/dist/validate.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +83 -3
- package/schemas/manifest.schema.json +132 -0
- package/schemas/outline.schema.json +40 -0
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt builders. One function per stage, each returning the exact strings the
|
|
3
|
+
* model receives. The text is versioned by `PROMPT_VERSION` in `types.ts`:
|
|
4
|
+
* bumping it invalidates the outline/script cache on purpose.
|
|
5
|
+
*
|
|
6
|
+
* Rules that matter and are easy to lose in a refactor:
|
|
7
|
+
* - every prompt demands JSON only (no fences, no prose);
|
|
8
|
+
* - the model never sees the whole book at once (block -> outline -> chapter);
|
|
9
|
+
* - neighboring summaries are injected so cross-chapter references survive.
|
|
10
|
+
*/
|
|
11
|
+
const JSON_ONLY = '只输出 JSON,不要解释、不要 Markdown 围栏、不要前后缀。字段不得增删;缺值用 ""、[] 或 null。';
|
|
12
|
+
/** Stage P0a: find chapter boundaries inside one text block. */
|
|
13
|
+
export function chunkPrompt(input) {
|
|
14
|
+
return {
|
|
15
|
+
system: `你是长文档结构分析器,负责在正文块中找出适合分章的位置。${JSON_ONLY}`,
|
|
16
|
+
user: `## 输入
|
|
17
|
+
- 全文标题:${input.docTitle}
|
|
18
|
+
- 本块编号:${input.blockId}(第 ${input.blockIndex} 块 / 共 ${input.blockTotal} 块)
|
|
19
|
+
- 本块起始段落序号:${input.startParagraph}
|
|
20
|
+
- 本块正文(段落以 ###P<序号>### 开头):
|
|
21
|
+
|
|
22
|
+
${input.blockText}
|
|
23
|
+
|
|
24
|
+
## 规则
|
|
25
|
+
1. 切点只能落在语义完整的位置:一个论点结束、一个子话题切换、一个案例讲完。禁止在小节中间、列举中途、例子没讲完时切断。
|
|
26
|
+
2. 每块给出 1–3 个候选切点,按推荐度排序,优先选读者会自然停下来的地方。
|
|
27
|
+
3. 每个候选给出 afterParagraph(切点前最后一段的序号)与不超过 14 字的标题。
|
|
28
|
+
4. 标题用动宾或名词短语,不带序号,不带「本章」「第一部分」。
|
|
29
|
+
5. 若本块是连续叙事、没有自然断点,chapterCandidates 给空数组,这是合法输出。
|
|
30
|
+
6. summary 不超过 120 字,只陈述讲了什么,不做评价、不引申。
|
|
31
|
+
7. crossRefs 记录需要前文或后文才能理解的指代(如「上述三原则」)。
|
|
32
|
+
|
|
33
|
+
## 输出
|
|
34
|
+
{"blockId":"","summary":"","chapterCandidates":[{"afterParagraph":0,"title":"","confidence":0.8,"reason":""}],"crossRefs":[]}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Stage P0b: merge block results into one coverage-complete outline. */
|
|
38
|
+
export function outlinePrompt(input) {
|
|
39
|
+
return {
|
|
40
|
+
system: `你是长文档的章节规划器。根据分块结果整理章节大纲。${JSON_ONLY}`,
|
|
41
|
+
user: `## 输入
|
|
42
|
+
- 全文标题:${input.docTitle}
|
|
43
|
+
- 全文段落总数:${input.paragraphTotal}
|
|
44
|
+
- 全文总字数:${input.charTotal}
|
|
45
|
+
- 分块结果(JSON 数组):
|
|
46
|
+
|
|
47
|
+
${JSON.stringify(input.blocks, null, 2)}
|
|
48
|
+
|
|
49
|
+
## 规则
|
|
50
|
+
1. 必须覆盖全文:第一章从段落 0 开始,最后一章到段落 ${input.paragraphTotal - 1} 结束,相邻章节首尾相接,不留空隙、不重叠。
|
|
51
|
+
2. 章节数 3–20;每章 800–4000 字,若必然超出,在 note 里说明(例如「该案例不可拆」)。
|
|
52
|
+
3. 标题风格统一:不超过 14 字,动宾或名词短语,不带序号。
|
|
53
|
+
4. 每章 summary 不超过 80 字,供其他章节做上下文注入。
|
|
54
|
+
5. sourceRange 用 [起始段落序号, 结束段落序号],包含两端。
|
|
55
|
+
6. 候选切点间隔小于 400 字时合并为一个章节。
|
|
56
|
+
|
|
57
|
+
## 输出
|
|
58
|
+
{"title":"","chapters":[{"id":"ch-01","title":"","sourceRange":[0,0],"summary":"","estimatedChars":0,"crossRefs":[],"note":""}]}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Stage P1: turn written prose into a spoken script without changing facts. */
|
|
62
|
+
export function adaptPrompt(input) {
|
|
63
|
+
return {
|
|
64
|
+
system: `你是中文有声内容的稿件编辑,把「用来读的文章」改成「用来听的稿子」。${JSON_ONLY}`,
|
|
65
|
+
user: `## 输入
|
|
66
|
+
- 全文标题:${input.docTitle}
|
|
67
|
+
- 本章:第 ${input.chapterNo} 章 / 共 ${input.chapterTotal} 章 · ${input.chapterTitle}
|
|
68
|
+
- 上一章摘要:${input.prevSummary || '(无)'}
|
|
69
|
+
- 下一章摘要:${input.nextSummary || '(无)'}
|
|
70
|
+
- 风格档位:${input.style}(lecture / news / story / docs)
|
|
71
|
+
- 本章原文:
|
|
72
|
+
|
|
73
|
+
${input.text}
|
|
74
|
+
|
|
75
|
+
## 硬规则(违反任何一条都算失败)
|
|
76
|
+
1. 不改事实、不加观点、不删信息;不许补充原文没有的解释、数据、例子、评价。
|
|
77
|
+
2. 数字与专有名词保真,只展开读法:3.5% → 百分之三点五;1,280 元 → 一千二百八十元;5–10 分钟 → 五到十分钟。API、DSH、MP3 这类缩写保留原形,不翻译。
|
|
78
|
+
3. 单句不超过 40 字;每段不超过 4 句;一段只讲一件事。
|
|
79
|
+
4. 必须删除的书面残留:括号补充、脚注编号、参考文献标记、图表引用(见图 3)、Markdown 符号、表格竖线;代码块改写为「下面这段代码,这里略过」并在 risks 中记录。
|
|
80
|
+
5. 跨章指代要能独立听懂:出现「上一章说的」时,补不超过 12 字的名词短语点明它是什么,这不算加信息。
|
|
81
|
+
6. 风格档位只影响节奏,不影响事实与结构。
|
|
82
|
+
7. 段落之间用空行分隔,不加小标题、不加「欢迎收听」这类开场白。
|
|
83
|
+
|
|
84
|
+
## 输出
|
|
85
|
+
{"script":"","changes":[{"type":"number","from":"","to":"","reason":""}],"risks":[{"kind":"code","text":""}]}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Stage P3: split the script into subtitle units (timing is computed later). */
|
|
89
|
+
export function subtitlePrompt(script) {
|
|
90
|
+
return {
|
|
91
|
+
system: `你是字幕断句器,把口播稿切成一条条字幕。${JSON_ONLY}`,
|
|
92
|
+
user: `## 输入
|
|
93
|
+
${script}
|
|
94
|
+
|
|
95
|
+
## 规则
|
|
96
|
+
1. 每条字幕最多 2 行,每行不超过 18 个汉字(含标点),优先做成 1 行;两行之间用 \\n 分隔。
|
|
97
|
+
2. 在语义边界断开:主语后、谓语后、连词前。禁止把「的」「了」放在行首,禁止拆开数字与单位。
|
|
98
|
+
3. 一句过长可在逗号处拆成多条;不同句子的内容禁止合并到同一条。
|
|
99
|
+
4. 引号内尽量不拆;必须拆时在第二个引号后断开。
|
|
100
|
+
5. 去掉句末句号,保留问号、感叹号、省略号。
|
|
101
|
+
6. 不新增、不删除、不改写文字。
|
|
102
|
+
|
|
103
|
+
## 输出
|
|
104
|
+
{"units":[{"text":""}]}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Stage P4: audit information fidelity, not style. */
|
|
108
|
+
export function auditPrompt(input) {
|
|
109
|
+
return {
|
|
110
|
+
system: `你是稿件质检员。对比原文与口播稿,只报告信息层面的差异,不评价文风。${JSON_ONLY}`,
|
|
111
|
+
user: `原文:
|
|
112
|
+
${input.source}
|
|
113
|
+
|
|
114
|
+
口播稿:
|
|
115
|
+
${input.script}
|
|
116
|
+
|
|
117
|
+
## 判定标准
|
|
118
|
+
- high:信息丢失、事实改变、数字或单位错误、把不确定说成确定
|
|
119
|
+
- medium:语气改变导致含义偏移、指代不可理解、重要限定条件被弱化
|
|
120
|
+
|
|
121
|
+
## 规则
|
|
122
|
+
1. 最多 8 条,按 severity 降序;宁少勿凑。
|
|
123
|
+
2. 每条必须给 evidence.source(原文片段,不超过 30 字)与 evidence.script(口播片段,不超过 30 字);给不出的不要报。
|
|
124
|
+
3. 只报告,不改写。
|
|
125
|
+
4. 没有 high 且没有 medium 时,verdict 必须是 pass。
|
|
126
|
+
|
|
127
|
+
## 输出
|
|
128
|
+
{"verdict":"pass","issues":[{"severity":"high","kind":"omission","evidence":{"source":"","script":""},"fix":""}]}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Stage P6: repair a structurally invalid JSON response without rewriting it. */
|
|
132
|
+
export function repairPrompt(input) {
|
|
133
|
+
return {
|
|
134
|
+
system: '你上一次的输出不满足 JSON schema。只修结构,不改内容。只输出 JSON。',
|
|
135
|
+
user: `## 错误
|
|
136
|
+
${input.error}
|
|
137
|
+
|
|
138
|
+
## 上一条输出
|
|
139
|
+
${input.previous}
|
|
140
|
+
|
|
141
|
+
## 要求
|
|
142
|
+
1. 只修被指出的字段与路径,其余字段逐字符保留。
|
|
143
|
+
2. 不要解释、不要道歉、不要围栏。
|
|
144
|
+
3. 字段确实无法推断时用 null 或空字符串,不要编造。`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** Coverage assertion used after planning; exported so tests can reuse it. */
|
|
148
|
+
export function coverageErrors(outline, paragraphTotal) {
|
|
149
|
+
const errors = [];
|
|
150
|
+
const chapters = outline.chapters;
|
|
151
|
+
if (chapters.length === 0)
|
|
152
|
+
return ['outline has no chapters'];
|
|
153
|
+
const first = chapters[0];
|
|
154
|
+
if (first.sourceRange[0] !== 0)
|
|
155
|
+
errors.push(`first chapter starts at ${first.sourceRange[0]}, expected 0`);
|
|
156
|
+
const last = chapters[chapters.length - 1];
|
|
157
|
+
if (last.sourceRange[1] !== paragraphTotal - 1) {
|
|
158
|
+
errors.push(`last chapter ends at ${last.sourceRange[1]}, expected ${paragraphTotal - 1}`);
|
|
159
|
+
}
|
|
160
|
+
for (let i = 1; i < chapters.length; i += 1) {
|
|
161
|
+
const prev = chapters[i - 1];
|
|
162
|
+
const current = chapters[i];
|
|
163
|
+
if (current.sourceRange[0] !== prev.sourceRange[1] + 1) {
|
|
164
|
+
errors.push(`${current.id} starts at ${current.sourceRange[0]}, expected ${prev.sourceRange[1] + 1}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return errors;
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve-entry.d.ts","sourceRoot":"","sources":["../src/serve-entry.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Executable entry: `dsh-voiceover` (or `node dist/serve-entry.js`).
|
|
4
|
+
*
|
|
5
|
+
* Kept separate from `cli.ts` so importing the daemon in tests cannot start it
|
|
6
|
+
* as a side effect — a direct-execution check on `import.meta.url` is easy to
|
|
7
|
+
* get wrong across launchers and symlinks.
|
|
8
|
+
*/
|
|
9
|
+
import { main } from "./cli.js";
|
|
10
|
+
void main();
|
|
11
|
+
//# sourceMappingURL=serve-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve-entry.js","sourceRoot":"","sources":["../src/serve-entry.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAE/B,KAAK,IAAI,EAAE,CAAA"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Capability, ChapterStatus, EngineInfo } from './types.ts';
|
|
2
|
+
/** Project ids are the first hex characters of a content hash. */
|
|
3
|
+
export declare const PROJECT_ID: RegExp;
|
|
4
|
+
/** One chapter row. */
|
|
5
|
+
export interface SnapshotChapter {
|
|
6
|
+
id: string;
|
|
7
|
+
order: number;
|
|
8
|
+
title: string;
|
|
9
|
+
status: ChapterStatus;
|
|
10
|
+
durationMs?: number;
|
|
11
|
+
subtitles?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
/** Relative audio path; the caller decides whether it is servable. */
|
|
14
|
+
audio?: string;
|
|
15
|
+
/** Playback URL, only when the caller supplied a minting function. */
|
|
16
|
+
audioUrl?: string;
|
|
17
|
+
}
|
|
18
|
+
/** What both surfaces render/serve. */
|
|
19
|
+
export interface ProjectSnapshot {
|
|
20
|
+
projectId: string;
|
|
21
|
+
title: string;
|
|
22
|
+
done: number;
|
|
23
|
+
total: number;
|
|
24
|
+
/** Chapters that still need work. */
|
|
25
|
+
pending: number;
|
|
26
|
+
failed: number;
|
|
27
|
+
capability: Capability;
|
|
28
|
+
engine: EngineInfo;
|
|
29
|
+
chapters: SnapshotChapter[];
|
|
30
|
+
warnings: string[];
|
|
31
|
+
/** Total warnings recorded; `warnings` is capped. */
|
|
32
|
+
warningCount: number;
|
|
33
|
+
/** mtime of `state.json`: the last time the pipeline actually wrote progress. */
|
|
34
|
+
stateUpdatedAt: string;
|
|
35
|
+
/** When this snapshot was served; not a progress fact. */
|
|
36
|
+
servedAt: string;
|
|
37
|
+
}
|
|
38
|
+
/** Options for {@link projectSnapshot}. */
|
|
39
|
+
export interface SnapshotOptions {
|
|
40
|
+
/** Mints a playback URL for a chapter's audio file, when a route exists. */
|
|
41
|
+
audioUrlFor?: (filePath: string) => string | undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Read one project's persisted progress. Returns undefined for an unknown or
|
|
45
|
+
* malformed id, and never reads outside `<outputDir>/<projectId>`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function projectSnapshot(outputDir: string, projectId: string, options?: SnapshotOptions): Promise<ProjectSnapshot | undefined>;
|
|
48
|
+
//# sourceMappingURL=snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAGvE,kEAAkE;AAClE,eAAO,MAAM,UAAU,QAAqB,CAAA;AAE5C,uBAAuB;AACvB,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,aAAa,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,uCAAuC;AACvC,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,UAAU,CAAA;IACtB,MAAM,EAAE,UAAU,CAAA;IAClB,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,qDAAqD;IACrD,YAAY,EAAE,MAAM,CAAA;IACpB,iFAAiF;IACjF,cAAc,EAAE,MAAM,CAAA;IACtB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,2CAA2C;AAC3C,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;CACvD;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAkEtC"}
|
package/dist/snapshot.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One builder for the project snapshot, shared by the conversation panel and
|
|
3
|
+
* the daemon API. Reimplementing this per surface is how two views drift apart
|
|
4
|
+
* and start disagreeing about which chapters are done.
|
|
5
|
+
*/
|
|
6
|
+
import { stat } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { readJson } from "./util.js";
|
|
9
|
+
/** Project ids are the first hex characters of a content hash. */
|
|
10
|
+
export const PROJECT_ID = /^[0-9a-f]{8,24}$/;
|
|
11
|
+
/**
|
|
12
|
+
* Read one project's persisted progress. Returns undefined for an unknown or
|
|
13
|
+
* malformed id, and never reads outside `<outputDir>/<projectId>`.
|
|
14
|
+
*/
|
|
15
|
+
export async function projectSnapshot(outputDir, projectId, options = {}) {
|
|
16
|
+
if (!PROJECT_ID.test(projectId))
|
|
17
|
+
return undefined;
|
|
18
|
+
const stateFile = join(outputDir, projectId, 'state', 'state.json');
|
|
19
|
+
const state = await readJson(stateFile);
|
|
20
|
+
if (state === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
// The panel and the API show "last write" so a stalled run is visible; a
|
|
23
|
+
// request time here would always look fresh and hide exactly that condition.
|
|
24
|
+
let stateUpdatedAt = new Date(0).toISOString();
|
|
25
|
+
try {
|
|
26
|
+
stateUpdatedAt = (await stat(stateFile)).mtime.toISOString();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Keep the epoch fallback: "unknown" must not claim freshness.
|
|
30
|
+
}
|
|
31
|
+
const chapters = [...state.chapters].sort((a, b) => a.order - b.order);
|
|
32
|
+
const done = chapters.filter((chapter) => chapter.status === 'done').length;
|
|
33
|
+
const failed = chapters.filter((chapter) => chapter.status === 'failed').length;
|
|
34
|
+
return {
|
|
35
|
+
projectId,
|
|
36
|
+
title: state.outline.title,
|
|
37
|
+
done,
|
|
38
|
+
total: state.outline.chapters.length,
|
|
39
|
+
pending: Math.max(0, state.outline.chapters.length - done - failed),
|
|
40
|
+
failed,
|
|
41
|
+
// The state records what was true when planning started; the chapters know
|
|
42
|
+
// the truth now. Reporting the stale value would make the panel claim
|
|
43
|
+
// "estimated" while every chapter has exact timing.
|
|
44
|
+
capability: {
|
|
45
|
+
...state.capability,
|
|
46
|
+
timingQuality: chapters.length === 0
|
|
47
|
+
? state.capability.timingQuality
|
|
48
|
+
: done === 0
|
|
49
|
+
? 'none'
|
|
50
|
+
: chapters
|
|
51
|
+
.filter((chapter) => chapter.status === 'done')
|
|
52
|
+
.every((chapter) => chapter.audio?.timingQuality === 'exact')
|
|
53
|
+
? 'exact'
|
|
54
|
+
: 'estimated',
|
|
55
|
+
},
|
|
56
|
+
engine: state.engine,
|
|
57
|
+
chapters: chapters.map((chapter) => {
|
|
58
|
+
const row = {
|
|
59
|
+
id: chapter.id,
|
|
60
|
+
order: chapter.order,
|
|
61
|
+
title: chapter.title,
|
|
62
|
+
status: chapter.status,
|
|
63
|
+
...(chapter.audio === undefined ? {} : { durationMs: chapter.audio.durationMs, audio: chapter.audio.path }),
|
|
64
|
+
...(chapter.subtitles === undefined ? {} : { subtitles: chapter.subtitles.path }),
|
|
65
|
+
...(chapter.error === undefined ? {} : { error: chapter.error.code }),
|
|
66
|
+
};
|
|
67
|
+
// Playback URLs are minted per request: they expire, and they only exist
|
|
68
|
+
// while a route is mounted.
|
|
69
|
+
if (chapter.audio !== undefined && options.audioUrlFor !== undefined) {
|
|
70
|
+
const url = options.audioUrlFor(join(outputDir, projectId, chapter.audio.path));
|
|
71
|
+
if (url !== undefined)
|
|
72
|
+
row.audioUrl = url;
|
|
73
|
+
}
|
|
74
|
+
return row;
|
|
75
|
+
}),
|
|
76
|
+
warnings: state.warnings.slice(0, 5),
|
|
77
|
+
warningCount: state.warnings.length,
|
|
78
|
+
stateUpdatedAt,
|
|
79
|
+
servedAt: new Date().toISOString(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subtitle construction: unit splitting (rule-based fallback when the model is
|
|
3
|
+
* unavailable), timing alignment (word boundaries when available, proportional
|
|
4
|
+
* otherwise), and SRT serialization.
|
|
5
|
+
*
|
|
6
|
+
* Two invariants the rest of the system relies on:
|
|
7
|
+
* - units are screen-sized (<= 2 lines of <= 18 chars), not sentence-sized;
|
|
8
|
+
* - timing is monotonic, clamped to the audio, and never claims `exact` unless
|
|
9
|
+
* word offsets were actually available.
|
|
10
|
+
*/
|
|
11
|
+
import type { WordTiming } from './types.ts';
|
|
12
|
+
/** One subtitle screen. `\n` separates the two allowed lines. */
|
|
13
|
+
export interface SubtitleUnit {
|
|
14
|
+
text: string;
|
|
15
|
+
}
|
|
16
|
+
/** A unit with resolved timing. */
|
|
17
|
+
export interface TimedUnit {
|
|
18
|
+
text: string;
|
|
19
|
+
startMs: number;
|
|
20
|
+
endMs: number;
|
|
21
|
+
}
|
|
22
|
+
/** Minimum time a screen stays visible. */
|
|
23
|
+
export declare const MIN_UNIT_MS = 800;
|
|
24
|
+
/** Gap between consecutive screens. */
|
|
25
|
+
export declare const MIN_GAP_MS = 50;
|
|
26
|
+
/** Characters per line before wrapping. */
|
|
27
|
+
export declare const MAX_LINE_CHARS = 18;
|
|
28
|
+
/** Characters per screen across both lines. */
|
|
29
|
+
export declare const MAX_UNIT_CHARS: number;
|
|
30
|
+
/** Count characters that carry timing weight (letters, digits, CJK). */
|
|
31
|
+
export declare function spokenLength(text: string): number;
|
|
32
|
+
/**
|
|
33
|
+
* Rule-based splitter used when no model is available. It is deliberately
|
|
34
|
+
* conservative: a sentence that fits becomes one screen, otherwise it is split
|
|
35
|
+
* at commas, and finally hard-split at the character limit.
|
|
36
|
+
*/
|
|
37
|
+
export declare function splitUnitsFallback(text: string): SubtitleUnit[];
|
|
38
|
+
/**
|
|
39
|
+
* Assign start/end to every unit. With word timings the result is `exact`;
|
|
40
|
+
* without them it is proportional to spoken length and must be labelled
|
|
41
|
+
* `estimated` by the caller.
|
|
42
|
+
*/
|
|
43
|
+
export declare function alignUnits(units: readonly SubtitleUnit[], durationMs: number, words?: readonly WordTiming[]): {
|
|
44
|
+
units: TimedUnit[];
|
|
45
|
+
quality: 'exact' | 'estimated';
|
|
46
|
+
};
|
|
47
|
+
/** Render an SRT document (1-based indices, blank line after each cue). */
|
|
48
|
+
export declare function formatSrt(units: readonly TimedUnit[]): string;
|
|
49
|
+
//# sourceMappingURL=subtitle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subtitle.d.ts","sourceRoot":"","sources":["../src/subtitle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,iEAAiE;AACjE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;CACb;AAED,mCAAmC;AACnC,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd;AAED,2CAA2C;AAC3C,eAAO,MAAM,WAAW,MAAM,CAAA;AAC9B,uCAAuC;AACvC,eAAO,MAAM,UAAU,KAAK,CAAA;AAC5B,2CAA2C;AAC3C,eAAO,MAAM,cAAc,KAAK,CAAA;AAChC,+CAA+C;AAC/C,eAAO,MAAM,cAAc,QAAqB,CAAA;AAKhD,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMjD;AAQD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,EAAE,CAsD/D;AAsBD;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,SAAS,YAAY,EAAE,EAC9B,UAAU,EAAE,MAAM,EAClB,KAAK,CAAC,EAAE,SAAS,UAAU,EAAE,GAC5B;IAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,GAAG,WAAW,CAAA;CAAE,CA+CxD;AAaD,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,MAAM,CAK7D"}
|
package/dist/subtitle.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/** Minimum time a screen stays visible. */
|
|
2
|
+
export const MIN_UNIT_MS = 800;
|
|
3
|
+
/** Gap between consecutive screens. */
|
|
4
|
+
export const MIN_GAP_MS = 50;
|
|
5
|
+
/** Characters per line before wrapping. */
|
|
6
|
+
export const MAX_LINE_CHARS = 18;
|
|
7
|
+
/** Characters per screen across both lines. */
|
|
8
|
+
export const MAX_UNIT_CHARS = MAX_LINE_CHARS * 2;
|
|
9
|
+
/** Characters that must not start a subtitle line. */
|
|
10
|
+
const NO_LINE_START = new Set(['的', '了', '是', '在', '和', '也', '就', '而', '与', '及', '着', '过', '吗', '呢']);
|
|
11
|
+
/** Count characters that carry timing weight (letters, digits, CJK). */
|
|
12
|
+
export function spokenLength(text) {
|
|
13
|
+
let count = 0;
|
|
14
|
+
for (const char of text) {
|
|
15
|
+
if (/[\p{L}\p{N}]/u.test(char))
|
|
16
|
+
count += 1;
|
|
17
|
+
}
|
|
18
|
+
return count;
|
|
19
|
+
}
|
|
20
|
+
/** Split on sentence punctuation, keeping the punctuation with its sentence. */
|
|
21
|
+
function sentences(text) {
|
|
22
|
+
const parts = text.split(/(?<=[。!?!?;;\n])/u);
|
|
23
|
+
return parts.map((part) => part.trim()).filter((part) => part.length > 0);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Rule-based splitter used when no model is available. It is deliberately
|
|
27
|
+
* conservative: a sentence that fits becomes one screen, otherwise it is split
|
|
28
|
+
* at commas, and finally hard-split at the character limit.
|
|
29
|
+
*/
|
|
30
|
+
export function splitUnitsFallback(text) {
|
|
31
|
+
const units = [];
|
|
32
|
+
for (const sentence of sentences(text)) {
|
|
33
|
+
if (spokenLength(sentence) <= MAX_LINE_CHARS) {
|
|
34
|
+
units.push(sentence.replace(/[。..]$/u, ''));
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const clauses = sentence.split(/(?<=[,,、])/u).map((part) => part.trim()).filter(Boolean);
|
|
38
|
+
let buffer = '';
|
|
39
|
+
for (const clause of clauses) {
|
|
40
|
+
if (buffer.length === 0) {
|
|
41
|
+
buffer = clause;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (spokenLength(buffer) + spokenLength(clause) <= MAX_LINE_CHARS) {
|
|
45
|
+
buffer += clause;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
units.push(buffer);
|
|
49
|
+
buffer = clause;
|
|
50
|
+
}
|
|
51
|
+
if (buffer.length > 0)
|
|
52
|
+
units.push(buffer);
|
|
53
|
+
}
|
|
54
|
+
// Hard-split anything still too long, then merge back a no-start character.
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const unit of units) {
|
|
57
|
+
const clean = unit.trim();
|
|
58
|
+
if (clean.length === 0)
|
|
59
|
+
continue;
|
|
60
|
+
if (clean.length <= MAX_UNIT_CHARS) {
|
|
61
|
+
out.push(clean);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
for (let i = 0; i < clean.length; i += MAX_LINE_CHARS) {
|
|
65
|
+
out.push(clean.slice(i, i + MAX_LINE_CHARS).trim());
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const merged = [];
|
|
69
|
+
for (const unit of out) {
|
|
70
|
+
const previous = merged[merged.length - 1];
|
|
71
|
+
const first = unit[0] ?? '';
|
|
72
|
+
if (previous !== undefined && NO_LINE_START.has(first) && previous.length + unit.length <= MAX_UNIT_CHARS) {
|
|
73
|
+
merged[merged.length - 1] = previous + unit;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
merged.push(unit);
|
|
77
|
+
}
|
|
78
|
+
return merged
|
|
79
|
+
.map((text) => text.replace(/[。..]$/u, '').trim())
|
|
80
|
+
.filter((text) => text.length > 0)
|
|
81
|
+
.map((text) => ({ text }));
|
|
82
|
+
}
|
|
83
|
+
/** Expand word timings into per-character slots. */
|
|
84
|
+
function charSlots(words) {
|
|
85
|
+
const slots = [];
|
|
86
|
+
for (const word of words) {
|
|
87
|
+
const chars = [...word.text].filter((char) => /[\p{L}\p{N}]/u.test(char));
|
|
88
|
+
if (chars.length === 0)
|
|
89
|
+
continue;
|
|
90
|
+
const per = word.durationMs / chars.length;
|
|
91
|
+
for (let i = 0; i < chars.length; i += 1) {
|
|
92
|
+
slots.push({ startMs: word.offsetMs + i * per, durationMs: per });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return slots;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Assign start/end to every unit. With word timings the result is `exact`;
|
|
99
|
+
* without them it is proportional to spoken length and must be labelled
|
|
100
|
+
* `estimated` by the caller.
|
|
101
|
+
*/
|
|
102
|
+
export function alignUnits(units, durationMs, words) {
|
|
103
|
+
const total = units.reduce((sum, unit) => sum + spokenLength(unit.text), 0);
|
|
104
|
+
const slots = words !== undefined && words.length > 0 ? charSlots(words) : [];
|
|
105
|
+
const useWords = slots.length >= total && total > 0;
|
|
106
|
+
const quality = useWords ? 'exact' : 'estimated';
|
|
107
|
+
const raw = [];
|
|
108
|
+
let cursor = 0;
|
|
109
|
+
const perChar = total > 0 ? durationMs / total : 0;
|
|
110
|
+
for (const unit of units) {
|
|
111
|
+
const length = spokenLength(unit.text);
|
|
112
|
+
let startMs;
|
|
113
|
+
let endMs;
|
|
114
|
+
if (useWords) {
|
|
115
|
+
const first = slots[cursor];
|
|
116
|
+
const last = slots[Math.min(slots.length - 1, cursor + Math.max(1, length) - 1)];
|
|
117
|
+
startMs = first?.startMs ?? 0;
|
|
118
|
+
endMs = last === undefined ? startMs + MIN_UNIT_MS : last.startMs + last.durationMs;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
startMs = Math.round(cursor * perChar);
|
|
122
|
+
endMs = Math.round((cursor + Math.max(1, length)) * perChar);
|
|
123
|
+
}
|
|
124
|
+
raw.push({ text: unit.text, startMs, endMs });
|
|
125
|
+
cursor += Math.max(1, length);
|
|
126
|
+
}
|
|
127
|
+
// Enforce the readability rules in one pass, keeping order monotonic.
|
|
128
|
+
const timed = [];
|
|
129
|
+
for (const [index, unit] of raw.entries()) {
|
|
130
|
+
const previous = timed[index - 1];
|
|
131
|
+
let start = unit.startMs;
|
|
132
|
+
if (previous !== undefined && start < previous.endMs + MIN_GAP_MS) {
|
|
133
|
+
start = previous.endMs + MIN_GAP_MS;
|
|
134
|
+
}
|
|
135
|
+
let end = Math.max(unit.endMs, start + MIN_UNIT_MS);
|
|
136
|
+
const next = raw[index + 1];
|
|
137
|
+
if (next !== undefined && end + MIN_GAP_MS > next.startMs) {
|
|
138
|
+
end = Math.max(start + 1, next.startMs - MIN_GAP_MS);
|
|
139
|
+
}
|
|
140
|
+
if (end > durationMs)
|
|
141
|
+
end = Math.max(start + 1, durationMs);
|
|
142
|
+
timed.push({ text: unit.text, startMs: Math.max(0, Math.round(start)), endMs: Math.round(end) });
|
|
143
|
+
}
|
|
144
|
+
return { units: timed, quality };
|
|
145
|
+
}
|
|
146
|
+
/** Format a millisecond offset as `HH:MM:SS,mmm`. */
|
|
147
|
+
function timestamp(ms) {
|
|
148
|
+
const total = Math.max(0, Math.round(ms));
|
|
149
|
+
const hours = Math.floor(total / 3_600_000);
|
|
150
|
+
const minutes = Math.floor((total % 3_600_000) / 60_000);
|
|
151
|
+
const seconds = Math.floor((total % 60_000) / 1000);
|
|
152
|
+
const millis = total % 1000;
|
|
153
|
+
const pad = (value, width) => String(value).padStart(width, '0');
|
|
154
|
+
return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(seconds, 2)},${pad(millis, 3)}`;
|
|
155
|
+
}
|
|
156
|
+
/** Render an SRT document (1-based indices, blank line after each cue). */
|
|
157
|
+
export function formatSrt(units) {
|
|
158
|
+
return units
|
|
159
|
+
.map((unit, index) => `${index + 1}\n${timestamp(unit.startMs)} --> ${timestamp(unit.endMs)}\n${unit.text}`)
|
|
160
|
+
.join('\n\n')
|
|
161
|
+
.concat('\n');
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=subtitle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subtitle.js","sourceRoot":"","sources":["../src/subtitle.ts"],"names":[],"mappings":"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"}
|