@assistkick/create 1.7.0 → 1.8.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/dist/bin/create.js +0 -0
- package/package.json +9 -7
- package/templates/assistkick-product-system/.env.example +1 -0
- package/templates/assistkick-product-system/local.db +0 -0
- package/templates/assistkick-product-system/package.json +4 -2
- package/templates/assistkick-product-system/packages/backend/package.json +2 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +165 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +358 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +356 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +96 -1
- package/templates/assistkick-product-system/packages/backend/src/routes/graph.ts +1 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +43 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +200 -84
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +6 -3
- package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +53 -17
- package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +218 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +119 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +154 -0
- package/templates/assistkick-product-system/packages/backend/src/server.ts +81 -9
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +489 -0
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +416 -0
- package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.test.ts +189 -0
- package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.ts +182 -0
- package/templates/assistkick-product-system/packages/backend/src/services/init.ts +28 -78
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +16 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +73 -2
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +4 -4
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +87 -11
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +210 -69
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +210 -215
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +162 -0
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +148 -0
- package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +11 -5
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.test.ts +64 -0
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +134 -0
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.test.ts +256 -0
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +258 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_group_service.ts +106 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.test.ts +275 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.ts +222 -0
- package/templates/assistkick-product-system/packages/frontend/package-lock.json +3455 -0
- package/templates/assistkick-product-system/packages/frontend/package.json +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +8 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +456 -16
- package/templates/assistkick-product-system/packages/frontend/src/api/client_files.test.ts +172 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client_video.test.ts +238 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +307 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/CoherenceView.tsx +82 -66
- package/templates/assistkick-product-system/packages/frontend/src/components/CompositionPlaceholder.tsx +97 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/DesignSystemView.tsx +20 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +57 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +313 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeContextMenu.tsx +61 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +73 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +404 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +187 -56
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +71 -73
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphSettings.tsx +8 -8
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphView.tsx +1 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/InviteUserDialog.tsx +15 -11
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +202 -171
- package/templates/assistkick-product-system/packages/frontend/src/components/LoginPage.tsx +14 -14
- package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +54 -33
- package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +32 -49
- package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +43 -48
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +20 -14
- package/templates/assistkick-product-system/packages/frontend/src/components/UsersView.tsx +52 -52
- package/templates/assistkick-product-system/packages/frontend/src/components/VideoGallery.tsx +313 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/VideographyView.tsx +250 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/WorkflowsView.tsx +474 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/AccentBorderList.tsx +53 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/Button.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonGroup.tsx +29 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonShowcase.tsx +221 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/CardGlass.tsx +141 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/CompletionRing.tsx +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ContentCard.tsx +34 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/IconButton.tsx +74 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +103 -87
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +9 -188
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/Kbd.tsx +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KindBadge.tsx +21 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +81 -37
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/SidePanelShowcase.tsx +370 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/SideSheet.tsx +64 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/StatusDot.tsx +18 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCardPositionNode.tsx +36 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCycleCountNode.tsx +60 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/EndNode.tsx +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +189 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +123 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RunAgentNode.tsx +51 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/SetCardMetadataNode.tsx +53 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/StartNode.tsx +18 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/TransitionCardNode.tsx +59 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +335 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +634 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/autoLayout.ts +103 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/edgeColors.ts +35 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +208 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.test.ts +119 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +107 -0
- package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +13 -11
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useAutoSave.ts +75 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useToast.tsx +16 -3
- package/templates/assistkick-product-system/packages/frontend/src/pages/accept_invitation_page.tsx +30 -27
- package/templates/assistkick-product-system/packages/frontend/src/pages/forgot_password_page.tsx +18 -15
- package/templates/assistkick-product-system/packages/frontend/src/pages/register_page.tsx +21 -18
- package/templates/assistkick-product-system/packages/frontend/src/pages/reset_password_page.tsx +28 -25
- package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +1 -1
- package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +13 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +6 -3
- package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +4 -4
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +275 -3535
- package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.test.ts +167 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.ts +101 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.test.ts +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.ts +17 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.test.ts +145 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.ts +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.test.ts +4 -10
- package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.ts +19 -1
- package/templates/assistkick-product-system/packages/frontend/vite.config.ts +5 -0
- package/templates/assistkick-product-system/packages/shared/db/local.db +0 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0004_tidy_matthew_murdock.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0005_mysterious_falcon.sql +692 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0006_next_venom.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0007_deep_barracuda.sql +39 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0008_puzzling_hannibal_king.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0009_amused_beast.sql +8 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0010_spotty_moira_mactaggert.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0011_goofy_snowbird.sql +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0011_supreme_doctor_octopus.sql +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0013_reflective_prowler.sql +15 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0004_snapshot.json +921 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0005_snapshot.json +1042 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0006_snapshot.json +1101 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0007_snapshot.json +1336 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0008_snapshot.json +1275 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0009_snapshot.json +1327 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0010_snapshot.json +1393 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0011_snapshot.json +1436 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0013_snapshot.json +1538 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +113 -0
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +32 -7
- package/templates/assistkick-product-system/packages/shared/lib/constants.ts +9 -0
- package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +12 -4
- package/templates/assistkick-product-system/packages/shared/lib/graph.ts +5 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +1753 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +1281 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +211 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +43 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +13 -2
- package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +1 -1
- package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.test.ts +226 -0
- package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.ts +251 -0
- package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
- package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.test.ts +10 -0
- package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.ts +6 -0
- package/templates/assistkick-product-system/packages/video/Root.tsx +85 -0
- package/templates/assistkick-product-system/packages/video/components/email_scene.tsx +231 -0
- package/templates/assistkick-product-system/packages/video/components/outro_scene.tsx +153 -0
- package/templates/assistkick-product-system/packages/video/components/part_divider.tsx +90 -0
- package/templates/assistkick-product-system/packages/video/components/scene.tsx +226 -0
- package/templates/assistkick-product-system/packages/video/components/theme.ts +22 -0
- package/templates/assistkick-product-system/packages/video/components/title_scene.tsx +169 -0
- package/templates/assistkick-product-system/packages/video/components/video_split_layout.tsx +84 -0
- package/templates/assistkick-product-system/packages/video/compositions/.gitkeep +0 -0
- package/templates/assistkick-product-system/packages/video/index.ts +4 -0
- package/templates/assistkick-product-system/packages/video/package.json +28 -0
- package/templates/assistkick-product-system/packages/video/remotion.config.ts +11 -0
- package/templates/assistkick-product-system/packages/video/scripts/process_script.test.ts +326 -0
- package/templates/assistkick-product-system/packages/video/scripts/process_script.ts +630 -0
- package/templates/assistkick-product-system/packages/video/style.css +1 -0
- package/templates/assistkick-product-system/packages/video/tsconfig.json +18 -0
- package/templates/assistkick-product-system/tests/graph_legend.test.ts +2 -1
- package/templates/assistkick-product-system/tests/video_render_service.test.ts +179 -0
- package/templates/assistkick-product-system/tests/web_terminal.test.ts +219 -455
- package/templates/assistkick-product-system/tests/workflow_integration.test.ts +341 -0
- package/templates/skills/assistkick-developer/SKILL.md +3 -0
- package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
- package/templates/skills/product-system/graph.json +1890 -0
- package/templates/skills/product-system/kanban.json +304 -0
- package/templates/skills/product-system/nodes/comp_001.md +56 -0
- package/templates/skills/product-system/nodes/comp_002.md +57 -0
- package/templates/skills/product-system/nodes/data_001.md +51 -0
- package/templates/skills/product-system/nodes/data_002.md +40 -0
- package/templates/skills/product-system/nodes/data_004.md +38 -0
- package/templates/skills/product-system/nodes/dec_001.md +34 -0
- package/templates/skills/product-system/nodes/dec_016.md +32 -0
- package/templates/skills/product-system/nodes/feat_008.md +30 -0
- package/templates/skills/video-composition-agent/SKILL.md +232 -0
- package/templates/skills/video-script-writer/SKILL.md +136 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Processes a video script markdown file:
|
|
3
|
+
* - Parses :::narration and :::screencapture directives
|
|
4
|
+
* - Generates TTS audio via ElevenLabs for missing or changed narrations
|
|
5
|
+
* - Measures audio and video durations
|
|
6
|
+
* - Writes durations.json and hashes.json to the media directory
|
|
7
|
+
* - Reports missing video files
|
|
8
|
+
*
|
|
9
|
+
* Text changes are detected via SHA-256 hashes stored in hashes.json.
|
|
10
|
+
* When narration text is edited, the audio is regenerated on the next run.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* tsx scripts/process_script.ts <script.md> --project-id <id> --feature-id <id> [options]
|
|
14
|
+
*
|
|
15
|
+
* Options:
|
|
16
|
+
* --project-id ID Project ID (required — resolves media dir)
|
|
17
|
+
* --feature-id ID Feature ID (required — scopes media within project)
|
|
18
|
+
* --media-base DIR Override media base dir (default: /data/workspaces/<projectId>/media)
|
|
19
|
+
* --force Regenerate all audio files, even if they exist
|
|
20
|
+
* --voice NAME ElevenLabs voice name (default: George)
|
|
21
|
+
* --voice-id ID ElevenLabs voice ID (overrides --voice)
|
|
22
|
+
* --dry-run Show what would be generated without calling the API
|
|
23
|
+
* --json Output machine-readable JSON summary (for API integration)
|
|
24
|
+
* --list-voices List available ElevenLabs voices
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
28
|
+
import { createHash } from "crypto";
|
|
29
|
+
import { join, resolve } from "path";
|
|
30
|
+
import https from "https";
|
|
31
|
+
|
|
32
|
+
// ── ElevenLabs config ────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const API_BASE = "api.elevenlabs.io";
|
|
35
|
+
const DEFAULT_MODEL = "eleven_v3";
|
|
36
|
+
const DEFAULT_OUTPUT_FORMAT = "mp3_44100_128";
|
|
37
|
+
const DEFAULT_VOICE_ID = "tnSpp4vdxKPjI9w0GnoV"; // George
|
|
38
|
+
|
|
39
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export type Narration = { id: string; src: string; text: string };
|
|
42
|
+
export type ScreenCapture = { id: string; src: string; description: string };
|
|
43
|
+
|
|
44
|
+
export type TtsResult = {
|
|
45
|
+
processed: number;
|
|
46
|
+
generated: number;
|
|
47
|
+
skipped: number;
|
|
48
|
+
errors: string[];
|
|
49
|
+
durations: Record<string, number>;
|
|
50
|
+
audioBasePath: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type CliArgs = {
|
|
54
|
+
script: string | null;
|
|
55
|
+
projectId: string | null;
|
|
56
|
+
featureId: string | null;
|
|
57
|
+
mediaBase: string | null;
|
|
58
|
+
force: boolean;
|
|
59
|
+
voice: string | null;
|
|
60
|
+
voiceId: string | null;
|
|
61
|
+
dryRun: boolean;
|
|
62
|
+
json: boolean;
|
|
63
|
+
listVoices: boolean;
|
|
64
|
+
help: boolean;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type ProcessScriptOptions = {
|
|
68
|
+
scriptPath: string;
|
|
69
|
+
projectId: string;
|
|
70
|
+
featureId: string;
|
|
71
|
+
mediaBase?: string;
|
|
72
|
+
force?: boolean;
|
|
73
|
+
voiceId?: string;
|
|
74
|
+
voice?: string;
|
|
75
|
+
dryRun?: boolean;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ── Hashing ──────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
export function hashText(text: string): string {
|
|
81
|
+
return createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function loadHashes(hashesFile: string): Record<string, string> {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(readFileSync(hashesFile, "utf-8"));
|
|
87
|
+
} catch {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Parse :::narration fenced directives ─────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
export function parseNarrations(markdown: string): Narration[] {
|
|
95
|
+
const regex = /:::narration\s+(\S+)\s*\n([\s\S]*?):::/g;
|
|
96
|
+
const narrations: Narration[] = [];
|
|
97
|
+
let match;
|
|
98
|
+
while ((match = regex.exec(markdown)) !== null) {
|
|
99
|
+
const id = match[1];
|
|
100
|
+
narrations.push({
|
|
101
|
+
id,
|
|
102
|
+
src: `audio/${id}.mp3`,
|
|
103
|
+
text: match[2].trim().replace(/\n+/g, " "),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return narrations;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Parse :::screencapture fenced directives ─────────────────────────────────
|
|
110
|
+
|
|
111
|
+
export function parseScreenCaptures(markdown: string): ScreenCapture[] {
|
|
112
|
+
const regex = /:::screencapture\s+(\S+)\s*\n([\s\S]*?):::/g;
|
|
113
|
+
const captures: ScreenCapture[] = [];
|
|
114
|
+
let match;
|
|
115
|
+
while ((match = regex.exec(markdown)) !== null) {
|
|
116
|
+
const id = match[1];
|
|
117
|
+
captures.push({
|
|
118
|
+
id,
|
|
119
|
+
src: `video/${id}.mp4`,
|
|
120
|
+
description: match[2].trim().replace(/\n+/g, " "),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return captures;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── MP3 duration measurement (pure JS — no ffprobe) ──────────────────────────
|
|
127
|
+
|
|
128
|
+
const MP3_BITRATES: Record<string, number[]> = {
|
|
129
|
+
"11": [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448],
|
|
130
|
+
"12": [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],
|
|
131
|
+
"13": [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320],
|
|
132
|
+
"21": [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
|
|
133
|
+
"22": [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
|
|
134
|
+
"23": [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const MP3_SAMPLE_RATES: Record<string, number[]> = {
|
|
138
|
+
"1": [44100, 48000, 32000],
|
|
139
|
+
"2": [22050, 24000, 16000],
|
|
140
|
+
"25": [11025, 12000, 8000],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export function getAudioDuration(filePath: string): number | null {
|
|
144
|
+
try {
|
|
145
|
+
const buf = readFileSync(filePath);
|
|
146
|
+
const fileSize = buf.length;
|
|
147
|
+
|
|
148
|
+
// Skip ID3v2 tag if present
|
|
149
|
+
let offset = 0;
|
|
150
|
+
if (buf[0] === 0x49 && buf[1] === 0x44 && buf[2] === 0x33) {
|
|
151
|
+
const tagSize =
|
|
152
|
+
((buf[6] & 0x7f) << 21) |
|
|
153
|
+
((buf[7] & 0x7f) << 14) |
|
|
154
|
+
((buf[8] & 0x7f) << 7) |
|
|
155
|
+
(buf[9] & 0x7f);
|
|
156
|
+
offset = 10 + tagSize;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Find first valid MP3 frame
|
|
160
|
+
while (offset < fileSize - 4) {
|
|
161
|
+
if (buf[offset] === 0xff && (buf[offset + 1] & 0xe0) === 0xe0) {
|
|
162
|
+
const header =
|
|
163
|
+
(buf[offset] << 24) |
|
|
164
|
+
(buf[offset + 1] << 16) |
|
|
165
|
+
(buf[offset + 2] << 8) |
|
|
166
|
+
buf[offset + 3];
|
|
167
|
+
|
|
168
|
+
const versionBits = (header >> 19) & 0x03;
|
|
169
|
+
const layerBits = (header >> 17) & 0x03;
|
|
170
|
+
const bitrateIdx = (header >> 12) & 0x0f;
|
|
171
|
+
const sampleIdx = (header >> 10) & 0x03;
|
|
172
|
+
|
|
173
|
+
if (
|
|
174
|
+
versionBits === 1 ||
|
|
175
|
+
layerBits === 0 ||
|
|
176
|
+
bitrateIdx === 0 ||
|
|
177
|
+
bitrateIdx === 15 ||
|
|
178
|
+
sampleIdx === 3
|
|
179
|
+
) {
|
|
180
|
+
offset++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const versionKey =
|
|
185
|
+
versionBits === 3 ? "1" : versionBits === 2 ? "2" : "25";
|
|
186
|
+
const layerKey = 4 - layerBits;
|
|
187
|
+
const brKey = `${versionBits === 3 ? 1 : 2}${layerKey}`;
|
|
188
|
+
const bitrate = (MP3_BITRATES[brKey]?.[bitrateIdx] ?? 0) * 1000;
|
|
189
|
+
const sampleRate = MP3_SAMPLE_RATES[versionKey]?.[sampleIdx] ?? 0;
|
|
190
|
+
|
|
191
|
+
if (bitrate > 0 && sampleRate > 0) {
|
|
192
|
+
const audioBytes = fileSize - offset;
|
|
193
|
+
const duration = (audioBytes * 8) / bitrate;
|
|
194
|
+
return Math.round(duration * 100) / 100;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
offset++;
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── MP4 duration measurement (pure JS — reads mvhd atom) ─────────────────────
|
|
206
|
+
|
|
207
|
+
export function getMp4Duration(filePath: string): number | null {
|
|
208
|
+
try {
|
|
209
|
+
const buf = readFileSync(filePath);
|
|
210
|
+
const len = buf.length;
|
|
211
|
+
let offset = 0;
|
|
212
|
+
|
|
213
|
+
while (offset < len - 8) {
|
|
214
|
+
const size = buf.readUInt32BE(offset);
|
|
215
|
+
const type = buf.toString("ascii", offset + 4, offset + 8);
|
|
216
|
+
|
|
217
|
+
if (size < 8 || offset + size > len) break;
|
|
218
|
+
|
|
219
|
+
// Recurse into container atoms
|
|
220
|
+
if (type === "moov" || type === "trak" || type === "mdia") {
|
|
221
|
+
offset += 8;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (type === "mvhd") {
|
|
226
|
+
const version = buf[offset + 8];
|
|
227
|
+
let timescale: number;
|
|
228
|
+
let duration: number;
|
|
229
|
+
|
|
230
|
+
if (version === 0) {
|
|
231
|
+
timescale = buf.readUInt32BE(offset + 20);
|
|
232
|
+
duration = buf.readUInt32BE(offset + 24);
|
|
233
|
+
} else {
|
|
234
|
+
timescale = buf.readUInt32BE(offset + 28);
|
|
235
|
+
const high = buf.readUInt32BE(offset + 32);
|
|
236
|
+
const low = buf.readUInt32BE(offset + 36);
|
|
237
|
+
duration = high * 0x100000000 + low;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (timescale > 0) {
|
|
241
|
+
return Math.round((duration / timescale) * 100) / 100;
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
offset += size;
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── ElevenLabs API ───────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
function loadApiKey(): string {
|
|
257
|
+
const key = process.env.ELEVENLABS_API_KEY;
|
|
258
|
+
if (key) return key;
|
|
259
|
+
|
|
260
|
+
console.error("Error: ELEVENLABS_API_KEY environment variable not set.");
|
|
261
|
+
console.error("Set it with: export ELEVENLABS_API_KEY=your_key_here");
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function apiRequest(
|
|
266
|
+
path: string,
|
|
267
|
+
apiKey: string,
|
|
268
|
+
method = "GET",
|
|
269
|
+
data: Record<string, unknown> | null = null,
|
|
270
|
+
): Promise<Buffer> {
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
const headers: Record<string, string> = { "xi-api-key": apiKey };
|
|
273
|
+
if (data) headers["Content-Type"] = "application/json";
|
|
274
|
+
|
|
275
|
+
const req = https.request(
|
|
276
|
+
{ hostname: API_BASE, path: `/v1${path}`, method, headers },
|
|
277
|
+
(res) => {
|
|
278
|
+
const chunks: Buffer[] = [];
|
|
279
|
+
res.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
280
|
+
res.on("end", () => {
|
|
281
|
+
const body = Buffer.concat(chunks);
|
|
282
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
283
|
+
reject(
|
|
284
|
+
new Error(`API error ${res.statusCode}: ${body.toString()}`),
|
|
285
|
+
);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
resolve(body);
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
req.on("error", reject);
|
|
293
|
+
if (data) req.write(JSON.stringify(data));
|
|
294
|
+
req.end();
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function listVoices(apiKey: string) {
|
|
299
|
+
const body = await apiRequest("/voices", apiKey);
|
|
300
|
+
const data = JSON.parse(body.toString());
|
|
301
|
+
console.log("Name".padEnd(30) + "Voice ID".padEnd(30) + "Category");
|
|
302
|
+
console.log("-".repeat(80));
|
|
303
|
+
for (const voice of data.voices ?? []) {
|
|
304
|
+
console.log(
|
|
305
|
+
(voice.name ?? "").padEnd(30) +
|
|
306
|
+
(voice.voice_id ?? "").padEnd(30) +
|
|
307
|
+
(voice.category ?? ""),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function resolveVoice(
|
|
313
|
+
voiceName: string,
|
|
314
|
+
apiKey: string,
|
|
315
|
+
): Promise<string> {
|
|
316
|
+
const body = await apiRequest("/voices", apiKey);
|
|
317
|
+
const data = JSON.parse(body.toString());
|
|
318
|
+
for (const voice of data.voices ?? []) {
|
|
319
|
+
if ((voice.name ?? "").toLowerCase() === voiceName.toLowerCase()) {
|
|
320
|
+
return voice.voice_id;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
throw new Error(`Voice '${voiceName}' not found. Use --list-voices to see available voices.`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function generateTTS(
|
|
327
|
+
text: string,
|
|
328
|
+
voiceId: string,
|
|
329
|
+
apiKey: string,
|
|
330
|
+
outputPath: string,
|
|
331
|
+
) {
|
|
332
|
+
const { dirname: dirName } = await import("path");
|
|
333
|
+
const audio = await apiRequest(
|
|
334
|
+
`/text-to-speech/${voiceId}?output_format=${DEFAULT_OUTPUT_FORMAT}`,
|
|
335
|
+
apiKey,
|
|
336
|
+
"POST",
|
|
337
|
+
{ text, model_id: DEFAULT_MODEL },
|
|
338
|
+
);
|
|
339
|
+
mkdirSync(dirName(outputPath), { recursive: true });
|
|
340
|
+
writeFileSync(outputPath, audio);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── Core processing function (used by both CLI and API) ─────────────────────
|
|
344
|
+
|
|
345
|
+
export async function processScript(opts: ProcessScriptOptions): Promise<TtsResult> {
|
|
346
|
+
const {
|
|
347
|
+
scriptPath,
|
|
348
|
+
projectId,
|
|
349
|
+
featureId,
|
|
350
|
+
mediaBase,
|
|
351
|
+
force = false,
|
|
352
|
+
voiceId: voiceIdOpt,
|
|
353
|
+
voice,
|
|
354
|
+
dryRun = false,
|
|
355
|
+
} = opts;
|
|
356
|
+
|
|
357
|
+
const resolvedPath = resolve(scriptPath);
|
|
358
|
+
if (!existsSync(resolvedPath)) {
|
|
359
|
+
throw new Error(`Script file not found: ${resolvedPath}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const mediaDir = mediaBase
|
|
363
|
+
? join(mediaBase, featureId)
|
|
364
|
+
: join("/data/workspaces", projectId, "media", featureId);
|
|
365
|
+
const audioDir = join(mediaDir, "audio");
|
|
366
|
+
const videoDir = join(mediaDir, "video");
|
|
367
|
+
const durationsFile = join(audioDir, "durations.json");
|
|
368
|
+
const videoDurationsFile = join(mediaDir, "video-durations.json");
|
|
369
|
+
const hashesFile = join(audioDir, "hashes.json");
|
|
370
|
+
|
|
371
|
+
const markdown = readFileSync(resolvedPath, "utf-8");
|
|
372
|
+
const narrations = parseNarrations(markdown);
|
|
373
|
+
const screenCaptures = parseScreenCaptures(markdown);
|
|
374
|
+
|
|
375
|
+
if (narrations.length === 0) {
|
|
376
|
+
throw new Error("No :::narration directives found in the script.");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Check for missing video files and measure video durations
|
|
380
|
+
const videoDurations: Record<string, number> = {};
|
|
381
|
+
if (screenCaptures.length > 0) {
|
|
382
|
+
for (const c of screenCaptures) {
|
|
383
|
+
const videoPath = join(videoDir, `${c.id}.mp4`);
|
|
384
|
+
if (!existsSync(videoPath)) continue;
|
|
385
|
+
const dur = getMp4Duration(videoPath);
|
|
386
|
+
if (dur !== null) {
|
|
387
|
+
videoDurations[c.id] = dur;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
mkdirSync(audioDir, { recursive: true });
|
|
393
|
+
|
|
394
|
+
const savedHashes = loadHashes(hashesFile);
|
|
395
|
+
const newHashes: Record<string, string> = {};
|
|
396
|
+
const errors: string[] = [];
|
|
397
|
+
|
|
398
|
+
const toGenerate: Narration[] = [];
|
|
399
|
+
let skipped = 0;
|
|
400
|
+
for (const n of narrations) {
|
|
401
|
+
const audioPath = join(audioDir, `${n.id}.mp3`);
|
|
402
|
+
const exists = existsSync(audioPath);
|
|
403
|
+
const currentHash = hashText(n.text);
|
|
404
|
+
newHashes[n.id] = currentHash;
|
|
405
|
+
const hashChanged = savedHashes[n.id] !== currentHash;
|
|
406
|
+
|
|
407
|
+
if (!exists || force || hashChanged) {
|
|
408
|
+
toGenerate.push(n);
|
|
409
|
+
} else {
|
|
410
|
+
skipped++;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
let generated = 0;
|
|
415
|
+
if (toGenerate.length > 0 && !dryRun) {
|
|
416
|
+
const apiKey = process.env.ELEVENLABS_API_KEY;
|
|
417
|
+
if (!apiKey) {
|
|
418
|
+
throw new Error("ELEVENLABS_API_KEY environment variable not set.");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
let finalVoiceId = DEFAULT_VOICE_ID;
|
|
422
|
+
if (voiceIdOpt) {
|
|
423
|
+
finalVoiceId = voiceIdOpt;
|
|
424
|
+
} else if (voice) {
|
|
425
|
+
finalVoiceId = await resolveVoice(voice, apiKey);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
for (const n of toGenerate) {
|
|
429
|
+
const audioPath = join(audioDir, `${n.id}.mp3`);
|
|
430
|
+
try {
|
|
431
|
+
await generateTTS(n.text, finalVoiceId, apiKey, audioPath);
|
|
432
|
+
generated++;
|
|
433
|
+
} catch (err) {
|
|
434
|
+
errors.push(`${n.id}: ${(err as Error).message}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Save text hashes
|
|
440
|
+
writeFileSync(hashesFile, JSON.stringify(newHashes, null, 2));
|
|
441
|
+
|
|
442
|
+
// Compute audio durations
|
|
443
|
+
const durations: Record<string, number> = {};
|
|
444
|
+
for (const n of narrations) {
|
|
445
|
+
const audioPath = join(audioDir, `${n.id}.mp3`);
|
|
446
|
+
const dur = getAudioDuration(audioPath);
|
|
447
|
+
if (dur !== null) {
|
|
448
|
+
durations[n.id] = dur;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
writeFileSync(durationsFile, JSON.stringify(durations, null, 2));
|
|
453
|
+
|
|
454
|
+
// Write video durations
|
|
455
|
+
if (Object.keys(videoDurations).length > 0) {
|
|
456
|
+
writeFileSync(videoDurationsFile, JSON.stringify(videoDurations, null, 2));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
processed: narrations.length,
|
|
461
|
+
generated,
|
|
462
|
+
skipped,
|
|
463
|
+
errors,
|
|
464
|
+
durations,
|
|
465
|
+
audioBasePath: audioDir,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ── CLI ──────────────────────────────────────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
function parseArgs(argv: string[]): CliArgs {
|
|
472
|
+
const args: CliArgs = {
|
|
473
|
+
script: null,
|
|
474
|
+
projectId: null,
|
|
475
|
+
featureId: null,
|
|
476
|
+
mediaBase: null,
|
|
477
|
+
force: false,
|
|
478
|
+
voice: null,
|
|
479
|
+
voiceId: null,
|
|
480
|
+
dryRun: false,
|
|
481
|
+
json: false,
|
|
482
|
+
listVoices: false,
|
|
483
|
+
help: false,
|
|
484
|
+
};
|
|
485
|
+
let i = 0;
|
|
486
|
+
while (i < argv.length) {
|
|
487
|
+
const arg = argv[i];
|
|
488
|
+
switch (arg) {
|
|
489
|
+
case "--project-id":
|
|
490
|
+
args.projectId = argv[++i];
|
|
491
|
+
break;
|
|
492
|
+
case "--feature-id":
|
|
493
|
+
args.featureId = argv[++i];
|
|
494
|
+
break;
|
|
495
|
+
case "--media-base":
|
|
496
|
+
args.mediaBase = argv[++i];
|
|
497
|
+
break;
|
|
498
|
+
case "--force":
|
|
499
|
+
args.force = true;
|
|
500
|
+
break;
|
|
501
|
+
case "--voice":
|
|
502
|
+
args.voice = argv[++i];
|
|
503
|
+
break;
|
|
504
|
+
case "--voice-id":
|
|
505
|
+
args.voiceId = argv[++i];
|
|
506
|
+
break;
|
|
507
|
+
case "--dry-run":
|
|
508
|
+
args.dryRun = true;
|
|
509
|
+
break;
|
|
510
|
+
case "--json":
|
|
511
|
+
args.json = true;
|
|
512
|
+
break;
|
|
513
|
+
case "--list-voices":
|
|
514
|
+
args.listVoices = true;
|
|
515
|
+
break;
|
|
516
|
+
case "-h":
|
|
517
|
+
case "--help":
|
|
518
|
+
args.help = true;
|
|
519
|
+
break;
|
|
520
|
+
default:
|
|
521
|
+
if (!arg.startsWith("-") && !args.script) {
|
|
522
|
+
args.script = arg;
|
|
523
|
+
}
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
i++;
|
|
527
|
+
}
|
|
528
|
+
return args;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function printHelp() {
|
|
532
|
+
console.log(`Usage: tsx scripts/process_script.ts <script.md> --project-id <id> --feature-id <id> [options]
|
|
533
|
+
|
|
534
|
+
Processes a video script: generates TTS audio, measures audio and video
|
|
535
|
+
durations, checks for missing video files, and writes duration JSON files.
|
|
536
|
+
|
|
537
|
+
Arguments:
|
|
538
|
+
script.md Path to script markdown file
|
|
539
|
+
|
|
540
|
+
Required:
|
|
541
|
+
--project-id ID Project ID (resolves media directory)
|
|
542
|
+
--feature-id ID Feature ID (scopes media within project)
|
|
543
|
+
|
|
544
|
+
Options:
|
|
545
|
+
--media-base DIR Override media base dir (default: /data/workspaces/<projectId>/media)
|
|
546
|
+
--force Regenerate all audio, even if files exist
|
|
547
|
+
--voice NAME ElevenLabs voice name (default: George)
|
|
548
|
+
--voice-id ID ElevenLabs voice ID (overrides --voice)
|
|
549
|
+
--dry-run Show what would be generated without calling the API
|
|
550
|
+
--json Output machine-readable JSON summary
|
|
551
|
+
--list-voices List available ElevenLabs voices
|
|
552
|
+
-h, --help Show this help`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
556
|
+
|
|
557
|
+
async function main() {
|
|
558
|
+
const args = parseArgs(process.argv.slice(2));
|
|
559
|
+
|
|
560
|
+
if (args.help) {
|
|
561
|
+
printHelp();
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (args.listVoices) {
|
|
566
|
+
const apiKey = loadApiKey();
|
|
567
|
+
await listVoices(apiKey);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (!args.script) {
|
|
572
|
+
console.error("Error: script path is required.");
|
|
573
|
+
printHelp();
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!args.projectId || !args.featureId) {
|
|
578
|
+
console.error("Error: --project-id and --feature-id are required.");
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
try {
|
|
583
|
+
const result = await processScript({
|
|
584
|
+
scriptPath: args.script,
|
|
585
|
+
projectId: args.projectId,
|
|
586
|
+
featureId: args.featureId,
|
|
587
|
+
mediaBase: args.mediaBase ?? undefined,
|
|
588
|
+
force: args.force,
|
|
589
|
+
voiceId: args.voiceId ?? undefined,
|
|
590
|
+
voice: args.voice ?? undefined,
|
|
591
|
+
dryRun: args.dryRun,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
if (args.json) {
|
|
595
|
+
console.log(JSON.stringify(result));
|
|
596
|
+
} else {
|
|
597
|
+
console.log(`Processed: ${result.processed} narration(s)`);
|
|
598
|
+
console.log(`Generated: ${result.generated} audio file(s)`);
|
|
599
|
+
console.log(`Skipped: ${result.skipped} (up to date)`);
|
|
600
|
+
if (result.errors.length > 0) {
|
|
601
|
+
console.log(`Errors: ${result.errors.length}`);
|
|
602
|
+
for (const err of result.errors) {
|
|
603
|
+
console.log(` - ${err}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
console.log(`\nAudio durations:`);
|
|
607
|
+
for (const [id, dur] of Object.entries(result.durations)) {
|
|
608
|
+
console.log(` ${id}: ${dur.toFixed(2)}s`);
|
|
609
|
+
}
|
|
610
|
+
console.log(`\nAudio base path: ${result.audioBasePath}`);
|
|
611
|
+
}
|
|
612
|
+
} catch (err) {
|
|
613
|
+
if (args.json) {
|
|
614
|
+
console.log(JSON.stringify({ error: (err as Error).message }));
|
|
615
|
+
process.exit(1);
|
|
616
|
+
} else {
|
|
617
|
+
console.error(`Error: ${(err as Error).message}`);
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Only run when executed directly (not when imported for testing)
|
|
624
|
+
const isMainModule =
|
|
625
|
+
import.meta.url === `file://${process.argv[1]}` ||
|
|
626
|
+
process.argv[1]?.endsWith("process_script.ts");
|
|
627
|
+
|
|
628
|
+
if (isMainModule) {
|
|
629
|
+
main();
|
|
630
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"jsxImportSource": "react",
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"allowSyntheticDefaultImports": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"types": ["react", "react-dom"]
|
|
15
|
+
},
|
|
16
|
+
"include": ["./**/*"],
|
|
17
|
+
"exclude": ["node_modules"]
|
|
18
|
+
}
|
|
@@ -90,7 +90,7 @@ describe('GraphLegend data logic', () => {
|
|
|
90
90
|
const { NODE_COLORS, EDGE_COLORS, TYPE_LABELS } = await import('../packages/frontend/src/constants/graph.js');
|
|
91
91
|
|
|
92
92
|
const expectedNodeTypes = [
|
|
93
|
-
'feature', 'component', 'data_entity', 'decision', 'tech_choice',
|
|
93
|
+
'feature', 'epic', 'component', 'data_entity', 'decision', 'tech_choice',
|
|
94
94
|
'non_functional_requirement', 'design_token', 'design_pattern',
|
|
95
95
|
'user_role', 'flow', 'assumption', 'open_question',
|
|
96
96
|
];
|
|
@@ -180,6 +180,7 @@ describe('GraphLegend data logic', () => {
|
|
|
180
180
|
|
|
181
181
|
const expected = {
|
|
182
182
|
feature: 'Feature',
|
|
183
|
+
epic: 'Epic',
|
|
183
184
|
component: 'Component',
|
|
184
185
|
data_entity: 'Data Entity',
|
|
185
186
|
decision: 'Decision',
|