plotlink-ows 1.0.32 → 1.2.94
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 +4 -0
- package/app/lib/agent-command.ts +85 -0
- package/app/lib/agent-readiness.ts +133 -0
- package/app/lib/apply-schema.ts +55 -0
- package/app/lib/bubble-text.ts +160 -0
- package/app/lib/cartoon-coach.ts +198 -0
- package/app/lib/cartoon-markdown.ts +83 -0
- package/app/lib/cartoon-prompt.ts +122 -0
- package/app/lib/cartoon-readiness.ts +811 -0
- package/app/lib/clean-image-sync.ts +245 -0
- package/app/lib/codex-images.ts +152 -0
- package/app/lib/cut-asset-diagnostics.ts +120 -0
- package/app/lib/cuts.ts +302 -0
- package/app/lib/fonts.ts +109 -0
- package/app/lib/generate-claude-md.ts +10 -3
- package/app/lib/generate-story-instructions.ts +731 -0
- package/app/lib/image-asset-validate.ts +123 -0
- package/app/lib/lettering-status.ts +133 -0
- package/app/lib/overlays.ts +637 -0
- package/app/lib/paths.ts +10 -0
- package/app/lib/public-title.ts +65 -0
- package/app/lib/publish.ts +16 -2
- package/app/lib/story-progress.ts +243 -0
- package/app/lib/terminal-protocol.ts +16 -0
- package/app/lib/terminal-redact.ts +50 -0
- package/app/prisma/schema.sql +25 -0
- package/app/routes/agent.ts +42 -0
- package/app/routes/codex-images.ts +67 -0
- package/app/routes/publish.ts +209 -28
- package/app/routes/stories.ts +961 -5
- package/app/routes/terminal.ts +383 -31
- package/app/server.ts +47 -12
- package/app/vite.config.ts +6 -0
- package/app/web/components/CartoonPreview.tsx +267 -0
- package/app/web/components/CartoonPublishPage.tsx +407 -0
- package/app/web/components/CartoonPublishPreview.tsx +121 -0
- package/app/web/components/CartoonStepGuide.tsx +90 -0
- package/app/web/components/CartoonWorkflowNav.tsx +68 -0
- package/app/web/components/CodexImportPicker.tsx +230 -0
- package/app/web/components/CutListPanel.tsx +1299 -0
- package/app/web/components/EpisodesPage.tsx +80 -0
- package/app/web/components/FinishEpisodePanel.tsx +151 -0
- package/app/web/components/Layout.tsx +7 -4
- package/app/web/components/LetteringEditor.tsx +1141 -0
- package/app/web/components/PreviewPanel.tsx +1017 -144
- package/app/web/components/Settings.tsx +63 -0
- package/app/web/components/StoriesPage.tsx +710 -33
- package/app/web/components/StoryBrowser.tsx +22 -14
- package/app/web/components/StoryInfoPage.tsx +266 -0
- package/app/web/components/StoryProgressPanel.tsx +516 -0
- package/app/web/components/TerminalPanel.tsx +233 -11
- package/app/web/components/WorkflowCoach.tsx +128 -0
- package/app/web/components/asset-image.tsx +114 -0
- package/app/web/components/asset-test-utils.ts +44 -0
- package/app/web/components/export-cut.ts +320 -0
- package/app/web/dist/assets/export-cut-nKQ_n2-J.js +1 -0
- package/app/web/dist/assets/index-BAZGwVwj.js +143 -0
- package/app/web/dist/assets/index-DoXH2OlP.css +32 -0
- package/app/web/dist/index.html +2 -2
- package/app/web/lib/cartoon-publish-summary.ts +43 -0
- package/app/web/lib/codex-import.ts +94 -0
- package/app/web/lib/image-compress.ts +53 -0
- package/app/web/lib/import-image.ts +58 -0
- package/app/web/lib/publish-helpers.ts +385 -0
- package/app/web/lib/upload-retry.ts +130 -0
- package/app/web/lib/verify-public-title.ts +105 -0
- package/app/web/styles.css +9 -0
- package/bin/plotlink-ows.js +53 -16
- package/bin/startup-plan.cjs +58 -0
- package/lib/genres.ts +92 -0
- package/package.json +60 -20
- package/scripts/gen-schema-sql.mjs +49 -0
- package/scripts/package-hygiene.mjs +116 -0
- package/scripts/preflight.mjs +173 -0
- package/scripts/start-smoke.mjs +128 -0
- package/app/node_modules/.prisma/local-client/client.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/client.js +0 -5
- package/app/node_modules/.prisma/local-client/default.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/default.js +0 -5
- package/app/node_modules/.prisma/local-client/edge.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/edge.js +0 -184
- package/app/node_modules/.prisma/local-client/index-browser.js +0 -173
- package/app/node_modules/.prisma/local-client/index.d.ts +0 -3304
- package/app/node_modules/.prisma/local-client/index.js +0 -207
- package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
- package/app/node_modules/.prisma/local-client/package.json +0 -183
- package/app/node_modules/.prisma/local-client/query_engine_bg.js +0 -2
- package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
- package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +0 -35
- package/app/node_modules/.prisma/local-client/runtime/edge.js +0 -35
- package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +0 -370
- package/app/node_modules/.prisma/local-client/runtime/index-browser.js +0 -17
- package/app/node_modules/.prisma/local-client/runtime/library.d.ts +0 -3982
- package/app/node_modules/.prisma/local-client/runtime/library.js +0 -147
- package/app/node_modules/.prisma/local-client/runtime/react-native.js +0 -84
- package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +0 -85
- package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +0 -38
- package/app/node_modules/.prisma/local-client/schema.prisma +0 -21
- package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +0 -5
- package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +0 -5
- package/app/node_modules/.prisma/local-client/wasm.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/wasm.js +0 -191
- package/app/web/dist/assets/index-B-2Ft7Yv.css +0 -32
- package/app/web/dist/assets/index-BFw-v-OZ.js +0 -134
- package/packages/cli/node_modules/commander/LICENSE +0 -22
- package/packages/cli/node_modules/commander/Readme.md +0 -1149
- package/packages/cli/node_modules/commander/esm.mjs +0 -16
- package/packages/cli/node_modules/commander/index.js +0 -24
- package/packages/cli/node_modules/commander/lib/argument.js +0 -149
- package/packages/cli/node_modules/commander/lib/command.js +0 -2662
- package/packages/cli/node_modules/commander/lib/error.js +0 -39
- package/packages/cli/node_modules/commander/lib/help.js +0 -709
- package/packages/cli/node_modules/commander/lib/option.js +0 -367
- package/packages/cli/node_modules/commander/lib/suggestSimilar.js +0 -101
- package/packages/cli/node_modules/commander/package-support.json +0 -16
- package/packages/cli/node_modules/commander/package.json +0 -82
- package/packages/cli/node_modules/commander/typings/esm.d.mts +0 -3
- package/packages/cli/node_modules/commander/typings/index.d.ts +0 -1045
- package/packages/cli/node_modules/resolve-from/index.d.ts +0 -31
- package/packages/cli/node_modules/resolve-from/index.js +0 -47
- package/packages/cli/node_modules/resolve-from/license +0 -9
- package/packages/cli/node_modules/resolve-from/package.json +0 -36
- package/packages/cli/node_modules/resolve-from/readme.md +0 -72
- package/packages/cli/node_modules/tsup/LICENSE +0 -21
- package/packages/cli/node_modules/tsup/README.md +0 -75
- package/packages/cli/node_modules/tsup/assets/cjs_shims.js +0 -13
- package/packages/cli/node_modules/tsup/assets/esm_shims.js +0 -9
- package/packages/cli/node_modules/tsup/assets/package.json +0 -3
- package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +0 -153
- package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +0 -42
- package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +0 -6
- package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +0 -352
- package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +0 -203
- package/packages/cli/node_modules/tsup/dist/cli-default.js +0 -12
- package/packages/cli/node_modules/tsup/dist/cli-main.js +0 -8
- package/packages/cli/node_modules/tsup/dist/cli-node.js +0 -14
- package/packages/cli/node_modules/tsup/dist/index.d.ts +0 -511
- package/packages/cli/node_modules/tsup/dist/index.js +0 -1711
- package/packages/cli/node_modules/tsup/dist/rollup.js +0 -6949
- package/packages/cli/node_modules/tsup/package.json +0 -99
- package/packages/cli/node_modules/tsup/schema.json +0 -362
- package/public/screenshot-1.png +0 -0
- package/public/screenshot-2.png +0 -0
- package/public/screenshot-3.png +0 -0
- package/scripts/e2e-verify.ts +0 -1100
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { sniffImageType, type SniffedType } from "./clean-image-sync";
|
|
4
|
+
|
|
5
|
+
// Shared filesystem validation for cartoon clean/final image assets. A single
|
|
6
|
+
// source of truth for "is this recorded/candidate relative path a real, valid
|
|
7
|
+
// WebP/JPEG file" — used by the sync/detect endpoints and by the stale-path
|
|
8
|
+
// detection that gates publish readiness (#302).
|
|
9
|
+
|
|
10
|
+
export const CLEAN_IMAGE_MAX_BYTES = 1024 * 1024;
|
|
11
|
+
export const CLEAN_IMAGE_VALID_EXT = new Set(["webp", "jpg", "jpeg"]);
|
|
12
|
+
|
|
13
|
+
/** Map an allowed file extension to the image type its content must match. */
|
|
14
|
+
export const CLEAN_IMAGE_EXT_TO_TYPE: Record<string, Exclude<SniffedType, "unknown">> = {
|
|
15
|
+
webp: "webp",
|
|
16
|
+
jpg: "jpeg",
|
|
17
|
+
jpeg: "jpeg",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate a relative asset path against the real filesystem. Returns `null`
|
|
22
|
+
* when the file exists and is a valid WebP/JPEG (regular file, allowed
|
|
23
|
+
* extension, <=1MB, magic-byte content matches the extension); otherwise a short
|
|
24
|
+
* reason string. Filesystem read only — never mutates anything.
|
|
25
|
+
*
|
|
26
|
+
* `"missing"` covers a non-existent path, a non-regular file, an unreadable
|
|
27
|
+
* file, or a path that escapes the story's `assets/` tree; the other reasons
|
|
28
|
+
* describe a present-but-invalid asset.
|
|
29
|
+
*/
|
|
30
|
+
export function imageAssetIssue(storyDir: string, relPath: string): string | null {
|
|
31
|
+
// Recorded cut asset paths come from cuts.json and must be a canonical
|
|
32
|
+
// RELATIVE path inside the story's assets/ tree. Reject non-canonical forms
|
|
33
|
+
// before any filesystem read so a recorded path cannot be trusted as a local
|
|
34
|
+
// asset on a technicality:
|
|
35
|
+
// - absolute paths (even ones that point inside assets/);
|
|
36
|
+
// - any `..` path segment (even when it resolves back inside assets/, e.g.
|
|
37
|
+
// "assets/plot-01/../evil.webp" → "assets/evil.webp").
|
|
38
|
+
// Then a resolved-boundary check rejects anything that still escapes assets/.
|
|
39
|
+
if (path.isAbsolute(relPath)) return "missing";
|
|
40
|
+
if (relPath.split(/[/\\]/).includes("..")) return "missing";
|
|
41
|
+
|
|
42
|
+
const assetsRoot = path.resolve(storyDir, "assets");
|
|
43
|
+
const abs = path.resolve(storyDir, relPath);
|
|
44
|
+
if (abs !== assetsRoot && !abs.startsWith(assetsRoot + path.sep)) return "missing";
|
|
45
|
+
|
|
46
|
+
if (!fs.existsSync(abs)) return "missing";
|
|
47
|
+
|
|
48
|
+
let stat: fs.Stats;
|
|
49
|
+
try {
|
|
50
|
+
stat = fs.statSync(abs);
|
|
51
|
+
} catch {
|
|
52
|
+
return "missing";
|
|
53
|
+
}
|
|
54
|
+
if (!stat.isFile()) return "missing";
|
|
55
|
+
|
|
56
|
+
const ext = path.extname(relPath).slice(1).toLowerCase();
|
|
57
|
+
if (!CLEAN_IMAGE_VALID_EXT.has(ext)) return `Unsupported extension .${ext}`;
|
|
58
|
+
if (stat.size > CLEAN_IMAGE_MAX_BYTES) return "File must be under 1MB";
|
|
59
|
+
|
|
60
|
+
// Sniff the real content so a text file (or a renamed/mismatched image) named
|
|
61
|
+
// `.webp`/`.jpg` cannot pass on extension alone.
|
|
62
|
+
let sniffed: SniffedType;
|
|
63
|
+
try {
|
|
64
|
+
const fd = fs.openSync(abs, "r");
|
|
65
|
+
try {
|
|
66
|
+
const head = Buffer.alloc(16);
|
|
67
|
+
const read = fs.readSync(fd, head, 0, 16, 0);
|
|
68
|
+
sniffed = sniffImageType(head.subarray(0, read));
|
|
69
|
+
} finally {
|
|
70
|
+
fs.closeSync(fd);
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
return "missing";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (sniffed === "unknown") return "not a valid image (content does not match WebP/JPEG/PNG)";
|
|
77
|
+
if (sniffed !== CLEAN_IMAGE_EXT_TO_TYPE[ext]) return `content does not match .${ext} extension`;
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** True when a relative asset path is a real, valid WebP/JPEG file on disk. */
|
|
82
|
+
export function isValidImageAsset(storyDir: string, relPath: string): boolean {
|
|
83
|
+
return imageAssetIssue(storyDir, relPath) === null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* True when a relative asset path points to a real PNG image on disk (#441).
|
|
88
|
+
* PNG is NOT a publishable clean format — but it is a normal intermediate that
|
|
89
|
+
* the writer converts to WebP/JPEG, so detection treats it as a conversion step
|
|
90
|
+
* rather than a hard "unsupported extension" error. Same traversal guards as
|
|
91
|
+
* `imageAssetIssue`; deliberately does NOT gate on the 1MB size limit (the
|
|
92
|
+
* browser conversion compresses, so an oversize PNG is still convertible).
|
|
93
|
+
*/
|
|
94
|
+
export function pngAssetExists(storyDir: string, relPath: string): boolean {
|
|
95
|
+
if (path.isAbsolute(relPath)) return false;
|
|
96
|
+
if (relPath.split(/[/\\]/).includes("..")) return false;
|
|
97
|
+
|
|
98
|
+
const assetsRoot = path.resolve(storyDir, "assets");
|
|
99
|
+
const abs = path.resolve(storyDir, relPath);
|
|
100
|
+
if (abs !== assetsRoot && !abs.startsWith(assetsRoot + path.sep)) return false;
|
|
101
|
+
|
|
102
|
+
if (!fs.existsSync(abs)) return false;
|
|
103
|
+
let stat: fs.Stats;
|
|
104
|
+
try {
|
|
105
|
+
stat = fs.statSync(abs);
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
if (!stat.isFile()) return false;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const fd = fs.openSync(abs, "r");
|
|
113
|
+
try {
|
|
114
|
+
const head = Buffer.alloc(16);
|
|
115
|
+
const read = fs.readSync(fd, head, 0, 16, 0);
|
|
116
|
+
return sniffImageType(head.subarray(0, read)) === "png";
|
|
117
|
+
} finally {
|
|
118
|
+
fs.closeSync(fd);
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Per-cut lettering status + "insert from script" helpers for the cartoon
|
|
2
|
+
// lettering editor guidance (#336). Pure and UI-agnostic so they can be unit
|
|
3
|
+
// tested and shared between the editor checklist and the insert-from-script
|
|
4
|
+
// panel. None of this changes the export model or publish readiness rules.
|
|
5
|
+
|
|
6
|
+
import type { Overlay } from "./overlays";
|
|
7
|
+
|
|
8
|
+
/** The cut fields the lettering guidance reads (a structural subset of Cut). */
|
|
9
|
+
export interface LetteringCut {
|
|
10
|
+
cleanImagePath?: string | null;
|
|
11
|
+
finalImagePath?: string | null;
|
|
12
|
+
exportedAt?: string | null;
|
|
13
|
+
uploadedUrl?: string | null;
|
|
14
|
+
uploadedCid?: string | null;
|
|
15
|
+
narration?: string;
|
|
16
|
+
sfx?: string;
|
|
17
|
+
dialogue?: { speaker: string; text: string }[];
|
|
18
|
+
overlays?: Overlay[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LetteringChecklist {
|
|
22
|
+
/** A clean (text-free) image has been recorded for the cut. */
|
|
23
|
+
hasCleanImage: boolean;
|
|
24
|
+
/** The cut plan carries script text (dialogue, narration, or SFX) to letter. */
|
|
25
|
+
hasScriptText: boolean;
|
|
26
|
+
/** How many overlays (bubbles/captions/SFX) have been placed. */
|
|
27
|
+
bubblesPlaced: number;
|
|
28
|
+
/** A final lettered image has been exported. */
|
|
29
|
+
exported: boolean;
|
|
30
|
+
/** An uploaded URL/CID is recorded for the cut. */
|
|
31
|
+
uploaded: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Summarize a single cut's lettering progress for the editor's status strip
|
|
36
|
+
* (#336): clean image present → script text available → bubbles placed →
|
|
37
|
+
* exported → uploaded. Read-only; derived straight from the cut record.
|
|
38
|
+
*
|
|
39
|
+
* `opts.staleExport` (#336, re1): when the writer has edited the overlays since
|
|
40
|
+
* the recorded export, the existing final image / uploaded URL no longer match
|
|
41
|
+
* what's on screen, so export & upload are reported as NOT done — the writer
|
|
42
|
+
* must re-export before those steps count again.
|
|
43
|
+
*/
|
|
44
|
+
export function cutLetteringChecklist(
|
|
45
|
+
cut: LetteringCut,
|
|
46
|
+
opts: { staleExport?: boolean } = {},
|
|
47
|
+
): LetteringChecklist {
|
|
48
|
+
const exported = !opts.staleExport && (!!cut.finalImagePath || !!cut.exportedAt);
|
|
49
|
+
const uploaded = !opts.staleExport && (!!cut.uploadedUrl || !!cut.uploadedCid);
|
|
50
|
+
return {
|
|
51
|
+
hasCleanImage: !!cut.cleanImagePath,
|
|
52
|
+
hasScriptText:
|
|
53
|
+
(cut.dialogue?.length ?? 0) > 0 || !!cut.narration?.trim() || !!cut.sfx?.trim(),
|
|
54
|
+
bubblesPlaced: cut.overlays?.length ?? 0,
|
|
55
|
+
exported,
|
|
56
|
+
uploaded,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Stable signature of an overlay set for change detection (#336). Captures only
|
|
62
|
+
* the fields that affect the rendered/exported image (type, geometry, text,
|
|
63
|
+
* speaker, tail), so reordering of unrelated metadata doesn't matter and any
|
|
64
|
+
* real edit changes the signature.
|
|
65
|
+
*/
|
|
66
|
+
export function overlaysSignature(overlays: Overlay[] | undefined): string {
|
|
67
|
+
return JSON.stringify(
|
|
68
|
+
(overlays ?? []).map((o) => [
|
|
69
|
+
o.type,
|
|
70
|
+
o.x,
|
|
71
|
+
o.y,
|
|
72
|
+
o.width,
|
|
73
|
+
o.height,
|
|
74
|
+
o.text,
|
|
75
|
+
o.speaker ?? "",
|
|
76
|
+
o.tailAnchor ?? null,
|
|
77
|
+
o.textStyle ?? null,
|
|
78
|
+
o.bubbleStyle ?? null,
|
|
79
|
+
]),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether a cut's recorded export/upload is stale because the overlays were
|
|
85
|
+
* edited since (#336, re1). Only meaningful once the cut has actually been
|
|
86
|
+
* exported or uploaded; compares the current overlays against the baseline that
|
|
87
|
+
* was on screen when the editor opened (already normalized the same way), so a
|
|
88
|
+
* load-time normalization is not mistaken for a user edit.
|
|
89
|
+
*/
|
|
90
|
+
export function isExportStale(opts: {
|
|
91
|
+
exported: boolean;
|
|
92
|
+
uploaded: boolean;
|
|
93
|
+
/** Signature of the overlays that match the recorded export (see overlaysSignature). */
|
|
94
|
+
baselineSig: string;
|
|
95
|
+
current: Overlay[] | undefined;
|
|
96
|
+
}): boolean {
|
|
97
|
+
if (!opts.exported && !opts.uploaded) return false;
|
|
98
|
+
return opts.baselineSig !== overlaysSignature(opts.current);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type ScriptLineType = "speech" | "narration" | "sfx";
|
|
102
|
+
|
|
103
|
+
/** A piece of the cut's script the writer can drop straight into an overlay. */
|
|
104
|
+
export interface ScriptLine {
|
|
105
|
+
type: ScriptLineType;
|
|
106
|
+
/** Speaker for a dialogue line; undefined for narration/SFX. */
|
|
107
|
+
speaker?: string;
|
|
108
|
+
text: string;
|
|
109
|
+
/** Stable key for list rendering / dedupe (type + index within its kind). */
|
|
110
|
+
key: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Flatten a cut's `cuts.json` script (dialogue lines, narration, SFX) into the
|
|
115
|
+
* ordered list the editor offers as one-click "insert into a bubble" actions
|
|
116
|
+
* (#336) — so a writer never has to hand-copy text out of the JSON. Empty
|
|
117
|
+
* pieces are skipped.
|
|
118
|
+
*/
|
|
119
|
+
export function cutScriptLines(cut: LetteringCut): ScriptLine[] {
|
|
120
|
+
const lines: ScriptLine[] = [];
|
|
121
|
+
(cut.dialogue ?? []).forEach((d, i) => {
|
|
122
|
+
if (d?.text?.trim()) {
|
|
123
|
+
lines.push({ type: "speech", speaker: d.speaker, text: d.text.trim(), key: `speech-${i}` });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
if (cut.narration?.trim()) {
|
|
127
|
+
lines.push({ type: "narration", text: cut.narration.trim(), key: "narration" });
|
|
128
|
+
}
|
|
129
|
+
if (cut.sfx?.trim()) {
|
|
130
|
+
lines.push({ type: "sfx", text: cut.sfx.trim(), key: "sfx" });
|
|
131
|
+
}
|
|
132
|
+
return lines;
|
|
133
|
+
}
|