@speclip/pi-subtitles 0.2.0 → 0.2.2
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/README.md
CHANGED
|
@@ -18,7 +18,7 @@ B-roll 只覆盖画面而保留 A-roll 主音频时,不会改变字幕时间
|
|
|
18
18
|
## 环境要求
|
|
19
19
|
|
|
20
20
|
- Node.js 22.19+
|
|
21
|
-
- Pi 0.84.1–0.
|
|
21
|
+
- Pi 0.84.1–0.85.x
|
|
22
22
|
- 一份含词级时间戳的 JSON 转录
|
|
23
23
|
|
|
24
24
|
## 安装与验证
|
|
@@ -115,7 +115,7 @@ subtitles_export {
|
|
|
115
115
|
|
|
116
116
|
工具会按项目画幅自动使用安全区样式,并返回 SRT / ASS 文件引用、SHA-256 和持久化导出回执。横屏使用固定 `1920×1080` ASS 设计坐标,竖屏使用 `1080×1920`;这与源视频是 1080p 还是 4K 无关,libass 会等比缩放。不要把 4K 源分辨率直接写成 ASS 的 `PlayRes`。
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
横屏和竖屏统一使用 `Source Han Sans SC Heavy`。白色字形不再附着黑色描边,而是使用向右下偏移的半透明黑色柔化阴影;横屏保持下方安全区,竖屏仍上移并扩大右边距,避开常见交互区。字体、阴影和几何参数只能在安全范围内微调。渲染环境必须真实安装该字体,不能依赖静默字体回退。
|
|
119
119
|
|
|
120
120
|
本包的完成状态是“字幕轨道已生成并校验”,不是“字幕已经烧进视频”。最后应把轨道交给支持字幕的渲染器,并检查实际成片中的同步、遮挡、安全区、字体回退和漏字。
|
|
121
121
|
|
|
@@ -140,7 +140,7 @@ npm run check
|
|
|
140
140
|
npm pack --dry-run
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
GitHub Release 标签必须与 `package.json` 完全一致,例如 `v0.2.
|
|
143
|
+
GitHub Release 标签必须与 `package.json` 完全一致,例如 `v0.2.1`。发布工作流会在标签快照上重新执行检查并通过 npm OIDC 发布。
|
|
144
144
|
|
|
145
145
|
## License
|
|
146
146
|
|
|
@@ -49,9 +49,11 @@ const assStyle = Type.Object({
|
|
|
49
49
|
fontSize: Type.Optional(Type.Number({ minimum: 1, maximum: 500 })),
|
|
50
50
|
primaryColour: Type.Optional(Type.String({ pattern: "^&H[0-9A-Fa-f]{8}$" })),
|
|
51
51
|
outlineColour: Type.Optional(Type.String({ pattern: "^&H[0-9A-Fa-f]{8}$" })),
|
|
52
|
+
shadowColour: Type.Optional(Type.String({ pattern: "^&H[0-9A-Fa-f]{8}$" })),
|
|
52
53
|
bold: Type.Optional(Type.Boolean()),
|
|
53
54
|
outline: Type.Optional(Type.Number({ minimum: 0, maximum: 20 })),
|
|
54
55
|
shadow: Type.Optional(Type.Number({ minimum: 0, maximum: 20 })),
|
|
56
|
+
blur: Type.Optional(Type.Number({ minimum: 0, maximum: 20 })),
|
|
55
57
|
alignment: Type.Optional(Type.Integer({ minimum: 1, maximum: 9 })),
|
|
56
58
|
marginL: Type.Optional(Type.Integer({ minimum: 0, maximum: 4_000 })),
|
|
57
59
|
marginR: Type.Optional(Type.Integer({ minimum: 0, maximum: 4_000 })),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@speclip/pi-subtitles",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Agent-reviewed subtitle timing, layout, and standard track export for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,11 +52,12 @@
|
|
|
52
52
|
]
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@earendil-works/pi-coding-agent": ">=0.84.1 <0.
|
|
55
|
+
"@earendil-works/pi-coding-agent": ">=0.84.1 <0.86.0",
|
|
56
56
|
"typebox": "^1.3.7"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
59
|
+
"@earendil-works/pi-coding-agent": "0.85.0",
|
|
60
|
+
"@earendil-works/pi-server": "0.85.0",
|
|
60
61
|
"@types/node": "22.20.1",
|
|
61
62
|
"typebox": "1.3.7",
|
|
62
63
|
"typescript": "7.0.2"
|
|
@@ -14,7 +14,7 @@ Use `pi-speech` for word timestamps and this package for final-timeline mapping,
|
|
|
14
14
|
5. Keep speech meaning intact. Correct ASR spelling or punctuation only with audio evidence. Do not delete content merely to make a line shorter. Preserve intentional fillers, emphasis, terminology, numbers, names, and sentence-ending tone.
|
|
15
15
|
6. Prefer one complete idea per cue and natural boundaries at strong punctuation or acoustic pauses. Landscape captions are always one line and must be split into another timed cue when too long; never insert `\n`. Portrait captions may use at most two balanced lines. Avoid one-character orphan cues and rapid flashes. Do not force every cue to an identical duration; subtitle timing follows the actual delivery rhythm.
|
|
16
16
|
7. If edits are needed, gather the complete cue list across all pages and call `subtitles_apply` with the current `expectedRevision` and a concrete `changeReason`. Never submit one paginated slice as if it were the full revision. If the revision changed, re-read and reconcile instead of overwriting another review.
|
|
17
|
-
8. Call `subtitles_export` with the exact reviewed revision. SRT is the portable timing/text track. Request ASS for final burn-in:
|
|
17
|
+
8. Call `subtitles_export` with the exact reviewed revision. SRT is the portable timing/text track. Request ASS for final burn-in: both layouts use `Source Han Sans SC Heavy`, white glyphs without a black outline, and a softened semi-transparent black shadow offset down and right. Do not replace `PlayRes` with the source video's 4K resolution. Verify that the renderer really resolves this font instead of silently falling back.
|
|
18
18
|
9. Keep rendering explicit. This package does not burn subtitles into video and does not invent FFmpeg arguments. Hand the returned SRT or ASS artifact and provenance receipt to a renderer that supports subtitle tracks, then visually review the actual final video for safe-area placement, occlusion, glyph fallback, sync, and missing text.
|
|
19
19
|
|
|
20
20
|
Never overwrite a transcript, subtitle track, or render. If the transcript hash changes, create a new subtitle project so timing decisions stay reproducible.
|
|
@@ -22,8 +22,8 @@ Timing values are working defaults, not hard editorial targets. Landscape single
|
|
|
22
22
|
|
|
23
23
|
## Safe-area presets
|
|
24
24
|
|
|
25
|
-
- Landscape uses canonical `1920×1080` ASS coordinates: font 80,
|
|
26
|
-
- Portrait uses canonical `1080×1920` ASS coordinates:
|
|
25
|
+
- Landscape uses canonical `1920×1080` ASS coordinates: `Source Han Sans SC Heavy` at font 80, no outline, shadow offset 4 with blur 0.6, left/right margins 120, bottom margin 120.
|
|
26
|
+
- Portrait uses canonical `1080×1920` ASS coordinates: the same Heavy font and shadow at font 72, left margin 90, right margin 200, bottom margin 460.
|
|
27
27
|
- Keep these design coordinates for 1080p, 2K, and 4K sources. libass scales them to the actual frame.
|
|
28
28
|
- Portrait's larger right and bottom margins avoid common right-side action controls and the lower interaction/caption region.
|
|
29
29
|
|
package/src/contracts.ts
CHANGED
|
@@ -53,9 +53,11 @@ export interface AssStyle {
|
|
|
53
53
|
fontSize: number;
|
|
54
54
|
primaryColour: string;
|
|
55
55
|
outlineColour: string;
|
|
56
|
+
shadowColour: string;
|
|
56
57
|
bold: boolean;
|
|
57
58
|
outline: number;
|
|
58
59
|
shadow: number;
|
|
60
|
+
blur: number;
|
|
59
61
|
alignment: number;
|
|
60
62
|
marginL: number;
|
|
61
63
|
marginR: number;
|
package/src/export.ts
CHANGED
|
@@ -5,17 +5,19 @@ import { resolveExistingWorkspaceFile, writeNewWorkspaceFile } from "./workspace
|
|
|
5
5
|
export const DEFAULT_ASS_STYLE: AssStyle = {
|
|
6
6
|
playResX: 1920,
|
|
7
7
|
playResY: 1080,
|
|
8
|
-
fontName: "
|
|
8
|
+
fontName: "Source Han Sans SC Heavy",
|
|
9
9
|
fontSize: 80,
|
|
10
10
|
primaryColour: "&H00FFFFFF",
|
|
11
11
|
outlineColour: "&H00000000",
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
shadowColour: "&H50000000",
|
|
13
|
+
bold: false,
|
|
14
|
+
outline: 0,
|
|
15
|
+
shadow: 4,
|
|
16
|
+
blur: 0.6,
|
|
15
17
|
alignment: 2,
|
|
16
18
|
marginL: 120,
|
|
17
19
|
marginR: 120,
|
|
18
|
-
marginV:
|
|
20
|
+
marginV: 120,
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
export const PORTRAIT_ASS_STYLE: AssStyle = {
|
|
@@ -38,12 +40,13 @@ export function resolveAssStyle(layoutPreset: SubtitleLayoutPreset, overrides: P
|
|
|
38
40
|
const resolved = { ...base, ...overrides };
|
|
39
41
|
const limits = layoutPreset === "portrait"
|
|
40
42
|
? { font: [60, 90], marginL: [60, 140], marginR: [160, 260], marginV: [430, 500] }
|
|
41
|
-
: { font: [64, 96], marginL: [90, 180], marginR: [90, 180], marginV: [
|
|
43
|
+
: { font: [64, 96], marginL: [90, 180], marginR: [90, 180], marginV: [110, 150] };
|
|
42
44
|
const within = (value: number, [minimum, maximum]: number[]) => Number.isFinite(value) && value >= minimum! && value <= maximum!;
|
|
43
|
-
if (
|
|
45
|
+
if (resolved.fontName !== base.fontName || resolved.bold !== base.bold || resolved.alignment !== 2
|
|
46
|
+
|| resolved.outline !== 0 || !within(resolved.shadow, [3, 5]) || !within(resolved.blur, [0.4, 1])
|
|
47
|
+
|| resolved.shadowColour !== base.shadowColour || !within(resolved.fontSize, limits.font)
|
|
44
48
|
|| !within(resolved.marginL, limits.marginL) || !within(resolved.marginR, limits.marginR)
|
|
45
|
-
|| !within(resolved.marginV, limits.marginV)
|
|
46
|
-
|| !within(resolved.shadow, [1, 5])) {
|
|
49
|
+
|| !within(resolved.marginV, limits.marginV)) {
|
|
47
50
|
throw new Error(`${layoutPreset} ASS style must preserve the readable safe-area preset`);
|
|
48
51
|
}
|
|
49
52
|
return resolved;
|
|
@@ -95,8 +98,9 @@ function assText(value: string): string {
|
|
|
95
98
|
export function renderAss(cues: SubtitleCue[], style: Partial<AssStyle> = {}): string {
|
|
96
99
|
assertCues(cues);
|
|
97
100
|
const resolved = { ...DEFAULT_ASS_STYLE, ...style };
|
|
98
|
-
const header = `[Script Info]\nScriptType: v4.00+\nPlayResX: ${resolved.playResX}\nPlayResY: ${resolved.playResY}\nWrapStyle: 2\nScaledBorderAndShadow: yes\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,${resolved.fontName},${resolved.fontSize},${resolved.primaryColour},${resolved.primaryColour},${resolved.outlineColour}
|
|
99
|
-
const
|
|
101
|
+
const header = `[Script Info]\nScriptType: v4.00+\nPlayResX: ${resolved.playResX}\nPlayResY: ${resolved.playResY}\nWrapStyle: 2\nScaledBorderAndShadow: yes\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,${resolved.fontName},${resolved.fontSize},${resolved.primaryColour},${resolved.primaryColour},${resolved.outlineColour},${resolved.shadowColour},${resolved.bold ? -1 : 0},0,0,0,100,100,0,0,1,${resolved.outline},${resolved.shadow},${resolved.alignment},${resolved.marginL},${resolved.marginR},${resolved.marginV},1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text`;
|
|
102
|
+
const blur = resolved.blur > 0 ? `{\\blur${resolved.blur}}` : "";
|
|
103
|
+
const events = cues.map((cue) => `Dialogue: 0,${assTime(cue.beginMs)},${assTime(cue.endMs)},Default,,0,0,0,,${blur}${assText(cue.text)}`);
|
|
100
104
|
return `${header}\n${events.join("\n")}\n`;
|
|
101
105
|
}
|
|
102
106
|
|