demofly 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.
Files changed (78) hide show
  1. package/dist/commands/auth/login.js +1 -1
  2. package/dist/commands/auth/login.js.map +1 -1
  3. package/dist/commands/demos/list.d.ts.map +1 -1
  4. package/dist/commands/demos/list.js +9 -6
  5. package/dist/commands/demos/list.js.map +1 -1
  6. package/dist/commands/generate.d.ts.map +1 -1
  7. package/dist/commands/generate.js +346 -35
  8. package/dist/commands/generate.js.map +1 -1
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +30 -1
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/commands/push.d.ts.map +1 -1
  13. package/dist/commands/push.js +11 -0
  14. package/dist/commands/push.js.map +1 -1
  15. package/dist/commands/render.d.ts.map +1 -1
  16. package/dist/commands/render.js +32 -4
  17. package/dist/commands/render.js.map +1 -1
  18. package/dist/commands/voices/index.d.ts +3 -0
  19. package/dist/commands/voices/index.d.ts.map +1 -0
  20. package/dist/commands/voices/index.js +11 -0
  21. package/dist/commands/voices/index.js.map +1 -0
  22. package/dist/commands/voices/list.d.ts +3 -0
  23. package/dist/commands/voices/list.d.ts.map +1 -0
  24. package/dist/commands/voices/list.js +72 -0
  25. package/dist/commands/voices/list.js.map +1 -0
  26. package/dist/commands/voices/select.d.ts +3 -0
  27. package/dist/commands/voices/select.d.ts.map +1 -0
  28. package/dist/commands/voices/select.js +79 -0
  29. package/dist/commands/voices/select.js.map +1 -0
  30. package/dist/index.js +2 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/lib/alignment.d.ts +47 -0
  33. package/dist/lib/alignment.d.ts.map +1 -0
  34. package/dist/lib/alignment.js +122 -0
  35. package/dist/lib/alignment.js.map +1 -0
  36. package/dist/lib/api-client.d.ts +2 -0
  37. package/dist/lib/api-client.d.ts.map +1 -1
  38. package/dist/lib/api-client.js +6 -2
  39. package/dist/lib/api-client.js.map +1 -1
  40. package/dist/lib/api-types.d.ts +15 -0
  41. package/dist/lib/api-types.d.ts.map +1 -0
  42. package/dist/lib/api-types.js +2 -0
  43. package/dist/lib/api-types.js.map +1 -0
  44. package/dist/lib/cloud-tts.d.ts +21 -0
  45. package/dist/lib/cloud-tts.d.ts.map +1 -0
  46. package/dist/lib/cloud-tts.js +42 -0
  47. package/dist/lib/cloud-tts.js.map +1 -0
  48. package/dist/lib/credentials.js +1 -1
  49. package/dist/lib/credentials.js.map +1 -1
  50. package/dist/lib/demo-config.d.ts +8 -0
  51. package/dist/lib/demo-config.d.ts.map +1 -0
  52. package/dist/lib/demo-config.js +29 -0
  53. package/dist/lib/demo-config.js.map +1 -0
  54. package/dist/lib/edit-proposals.d.ts +46 -0
  55. package/dist/lib/edit-proposals.d.ts.map +1 -0
  56. package/dist/lib/edit-proposals.js +159 -0
  57. package/dist/lib/edit-proposals.js.map +1 -0
  58. package/dist/lib/retiming.d.ts +19 -0
  59. package/dist/lib/retiming.d.ts.map +1 -0
  60. package/dist/lib/retiming.js +215 -0
  61. package/dist/lib/retiming.js.map +1 -0
  62. package/dist/lib/timestamps.d.ts +43 -0
  63. package/dist/lib/timestamps.d.ts.map +1 -0
  64. package/dist/lib/timestamps.js +199 -0
  65. package/dist/lib/timestamps.js.map +1 -0
  66. package/dist/lib/tts.d.ts +7 -4
  67. package/dist/lib/tts.d.ts.map +1 -1
  68. package/dist/lib/tts.js +15 -8
  69. package/dist/lib/tts.js.map +1 -1
  70. package/dist/lib/voice-config.d.ts +7 -0
  71. package/dist/lib/voice-config.d.ts.map +1 -0
  72. package/dist/lib/voice-config.js +29 -0
  73. package/dist/lib/voice-config.js.map +1 -0
  74. package/dist/lib/voice-resolver.d.ts +82 -0
  75. package/dist/lib/voice-resolver.d.ts.map +1 -0
  76. package/dist/lib/voice-resolver.js +108 -0
  77. package/dist/lib/voice-resolver.js.map +1 -0
  78. package/package.json +1 -1
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Demofly edit proposals — LLM-generated retiming instructions.
3
+ *
4
+ * Takes alignment.json and produces edit-decisions.json with per-segment
5
+ * retiming instructions that can be executed by ffmpeg.
6
+ *
7
+ * Uses Claude API to generate intelligent edit decisions based on drift
8
+ * data, hero moment awareness, and assembly best practices.
9
+ */
10
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+ import { debug } from "./logger.js";
13
+ /** Hard limits for retiming decisions. */
14
+ const LIMITS = {
15
+ maxSpeedup: 1.5,
16
+ maxSlowdown: 1.33,
17
+ maxFreezeFrameMs: 3000,
18
+ minConfidence: 0.7,
19
+ };
20
+ /**
21
+ * Generate edit proposals from alignment data.
22
+ *
23
+ * This produces deterministic, rule-based proposals without requiring an
24
+ * LLM call. The proposals follow the same format that an LLM would produce,
25
+ * making it easy to swap in LLM-powered proposals later.
26
+ *
27
+ * When an LLM API is available, use `generateLlmEditProposals()` instead
28
+ * for more nuanced decisions (e.g., choosing freeze_frame vs pad_silence
29
+ * based on visual content).
30
+ */
31
+ export function generateEditProposals(alignment, heroSceneIds = []) {
32
+ const allDecisions = [];
33
+ for (const beat of alignment.beats) {
34
+ const decision = proposeEdit(beat, heroSceneIds);
35
+ if (decision) {
36
+ allDecisions.push(decision);
37
+ }
38
+ }
39
+ // Split into applied (confidence >= threshold) and skipped
40
+ const applied = allDecisions.filter((d) => d.confidence >= LIMITS.minConfidence);
41
+ const skipped = allDecisions.filter((d) => d.confidence < LIMITS.minConfidence);
42
+ for (const s of skipped) {
43
+ debug(`Skipped edit for ${s.beatId}: ${s.editType} (confidence ${s.confidence})`);
44
+ }
45
+ return {
46
+ decisions: applied,
47
+ skipped,
48
+ summary: {
49
+ total: allDecisions.length,
50
+ applied: applied.length,
51
+ skipped: skipped.length,
52
+ },
53
+ };
54
+ }
55
+ /**
56
+ * Propose a single edit decision for a beat based on its alignment.
57
+ */
58
+ function proposeEdit(beat, heroSceneIds) {
59
+ const { driftMs, videoDurationMs, audioDurationMs, sceneId } = beat;
60
+ const isHero = heroSceneIds.includes(sceneId);
61
+ // No drift — no edit needed
62
+ if (beat.status === "aligned") {
63
+ return null;
64
+ }
65
+ // Video is longer than audio (positive drift) — speed up or trim
66
+ if (driftMs > 0) {
67
+ // Never speed up hero moments
68
+ if (isHero) {
69
+ // Pad audio with silence instead
70
+ return {
71
+ beatId: beat.beatId,
72
+ editType: "pad_silence",
73
+ segmentStartMs: 0,
74
+ segmentEndMs: videoDurationMs,
75
+ targetDurationMs: videoDurationMs,
76
+ rationale: `Hero scene: padding audio with ${driftMs}ms silence instead of speeding up video`,
77
+ confidence: 0.9,
78
+ };
79
+ }
80
+ const speedupRatio = videoDurationMs / audioDurationMs;
81
+ if (speedupRatio <= LIMITS.maxSpeedup) {
82
+ return {
83
+ beatId: beat.beatId,
84
+ editType: "speed_video",
85
+ segmentStartMs: 0,
86
+ segmentEndMs: videoDurationMs,
87
+ targetDurationMs: audioDurationMs,
88
+ rationale: `Video ${driftMs}ms longer than audio; ${speedupRatio.toFixed(2)}x speedup within limits`,
89
+ confidence: speedupRatio <= 1.2 ? 0.9 : 0.75,
90
+ };
91
+ }
92
+ // Too much speedup needed — trim silence instead
93
+ return {
94
+ beatId: beat.beatId,
95
+ editType: "trim_silence",
96
+ segmentStartMs: 0,
97
+ segmentEndMs: videoDurationMs,
98
+ targetDurationMs: audioDurationMs,
99
+ rationale: `Video ${driftMs}ms longer; speedup would exceed ${LIMITS.maxSpeedup}x limit, trimming silence instead`,
100
+ confidence: 0.65,
101
+ };
102
+ }
103
+ // Audio is longer than video (negative drift) — slow down or freeze
104
+ if (driftMs < 0) {
105
+ const absDrift = Math.abs(driftMs);
106
+ const slowdownRatio = audioDurationMs / videoDurationMs;
107
+ if (slowdownRatio <= LIMITS.maxSlowdown) {
108
+ return {
109
+ beatId: beat.beatId,
110
+ editType: "slow_video",
111
+ segmentStartMs: 0,
112
+ segmentEndMs: videoDurationMs,
113
+ targetDurationMs: audioDurationMs,
114
+ rationale: `Audio ${absDrift}ms longer than video; ${slowdownRatio.toFixed(2)}x slowdown within limits`,
115
+ confidence: slowdownRatio <= 1.15 ? 0.9 : 0.75,
116
+ };
117
+ }
118
+ // Too much slowdown — freeze frame
119
+ if (absDrift <= LIMITS.maxFreezeFrameMs) {
120
+ return {
121
+ beatId: beat.beatId,
122
+ editType: "freeze_frame",
123
+ segmentStartMs: videoDurationMs,
124
+ segmentEndMs: videoDurationMs,
125
+ targetDurationMs: absDrift,
126
+ rationale: `Audio ${absDrift}ms longer; freeze last frame for ${absDrift}ms to cover gap`,
127
+ confidence: absDrift <= 1500 ? 0.85 : 0.7,
128
+ };
129
+ }
130
+ // Critical drift — log but low confidence
131
+ return {
132
+ beatId: beat.beatId,
133
+ editType: "freeze_frame",
134
+ segmentStartMs: videoDurationMs,
135
+ segmentEndMs: videoDurationMs,
136
+ targetDurationMs: Math.min(absDrift, LIMITS.maxFreezeFrameMs),
137
+ rationale: `Critical drift: audio ${absDrift}ms longer; capped freeze at ${LIMITS.maxFreezeFrameMs}ms`,
138
+ confidence: 0.5,
139
+ };
140
+ }
141
+ return null;
142
+ }
143
+ /**
144
+ * Generate edit proposals and write to recordings/edit-decisions.json.
145
+ */
146
+ export function generateAndWriteEditProposals(projectDir, heroSceneIds = []) {
147
+ const alignmentPath = resolve(projectDir, "recordings", "alignment.json");
148
+ if (!existsSync(alignmentPath)) {
149
+ console.error("No alignment.json found. Run `demofly generate <name> --align` first.");
150
+ return null;
151
+ }
152
+ const alignment = JSON.parse(readFileSync(alignmentPath, "utf-8"));
153
+ const proposals = generateEditProposals(alignment, heroSceneIds);
154
+ const outputPath = resolve(projectDir, "recordings", "edit-decisions.json");
155
+ writeFileSync(outputPath, JSON.stringify(proposals, null, 2), "utf-8");
156
+ console.log(` Written: ${outputPath}`);
157
+ return proposals;
158
+ }
159
+ //# sourceMappingURL=edit-proposals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edit-proposals.js","sourceRoot":"","sources":["../../src/lib/edit-proposals.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Demofly retiming — translate edit decisions into ffmpeg filter commands.
3
+ *
4
+ * Segment-based retiming: split video → per-segment transforms → concatenate.
5
+ * Replaces simple stitchAudio() for the intelligent assembly pipeline.
6
+ */
7
+ import type { TimingData } from "./timing.js";
8
+ import type { EditDecisionsData } from "./edit-proposals.js";
9
+ /**
10
+ * Apply edit decisions to video segments and produce a retimed final video.
11
+ *
12
+ * Pipeline:
13
+ * 1. Split video into per-scene segments
14
+ * 2. Apply per-segment transforms (speed change, freeze frame, etc.)
15
+ * 3. Concatenate retimed segments
16
+ * 4. Mux with audio
17
+ */
18
+ export declare function retimeAndAssemble(videoPath: string, audioDir: string, timingData: TimingData, decisions: EditDecisionsData, recordingsDir: string): string | null;
19
+ //# sourceMappingURL=retiming.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retiming.d.ts","sourceRoot":"","sources":["../../src/lib/retiming.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAQ7D;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,UAAU,EACtB,SAAS,EAAE,iBAAiB,EAC5B,aAAa,EAAE,MAAM,GACpB,MAAM,GAAG,IAAI,CAwCf"}
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Demofly retiming — translate edit decisions into ffmpeg filter commands.
3
+ *
4
+ * Segment-based retiming: split video → per-segment transforms → concatenate.
5
+ * Replaces simple stitchAudio() for the intelligent assembly pipeline.
6
+ */
7
+ import { execSync } from "node:child_process";
8
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
9
+ import { resolve } from "node:path";
10
+ import { debug } from "./logger.js";
11
+ /**
12
+ * Apply edit decisions to video segments and produce a retimed final video.
13
+ *
14
+ * Pipeline:
15
+ * 1. Split video into per-scene segments
16
+ * 2. Apply per-segment transforms (speed change, freeze frame, etc.)
17
+ * 3. Concatenate retimed segments
18
+ * 4. Mux with audio
19
+ */
20
+ export function retimeAndAssemble(videoPath, audioDir, timingData, decisions, recordingsDir) {
21
+ const workDir = resolve(recordingsDir, ".retiming-work");
22
+ mkdirSync(workDir, { recursive: true });
23
+ try {
24
+ // Step 1: Split video into scene segments
25
+ console.log(" Splitting video into scene segments...");
26
+ const segments = splitIntoSegments(videoPath, timingData, workDir);
27
+ if (segments.length === 0) {
28
+ console.warn("No segments produced. Falling back to simple stitch.");
29
+ return null;
30
+ }
31
+ // Step 2: Apply edits to each segment
32
+ console.log(" Applying retiming edits...");
33
+ const retimed = applyEdits(segments, decisions, workDir);
34
+ // Step 3: Mux audio with retimed segments
35
+ console.log(" Muxing audio...");
36
+ const muxed = muxAudioToSegments(retimed, audioDir, workDir);
37
+ // Step 4: Concatenate all segments
38
+ console.log(" Concatenating final video...");
39
+ const outputPath = resolve(recordingsDir, "final.mp4");
40
+ const success = concatenateSegments(muxed, outputPath, workDir);
41
+ if (success) {
42
+ return outputPath;
43
+ }
44
+ return null;
45
+ }
46
+ finally {
47
+ // Clean up work directory
48
+ try {
49
+ execSync(`rm -rf "${workDir}"`, { stdio: "pipe" });
50
+ }
51
+ catch {
52
+ // Non-critical cleanup failure
53
+ }
54
+ }
55
+ }
56
+ /**
57
+ * Split video into per-scene segments based on timing data.
58
+ */
59
+ function splitIntoSegments(videoPath, timingData, workDir) {
60
+ const segments = [];
61
+ for (const scene of timingData.scenes) {
62
+ const startSec = scene.startMs / 1000;
63
+ const durationSec = (scene.endMs - scene.startMs) / 1000;
64
+ const outputPath = resolve(workDir, `${scene.sceneId}-raw.mp4`);
65
+ try {
66
+ execSync(`ffmpeg -y -i "${videoPath}" -ss ${startSec} -t ${durationSec} ` +
67
+ `-c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -an "${outputPath}"`, { encoding: "utf-8", timeout: 120_000, stdio: ["pipe", "pipe", "pipe"] });
68
+ segments.push({ path: outputPath, sceneId: scene.sceneId });
69
+ }
70
+ catch (error) {
71
+ const execError = error;
72
+ console.warn(` Warning: Failed to split ${scene.sceneId}:`);
73
+ if (execError.stderr) {
74
+ debug(execError.stderr);
75
+ }
76
+ }
77
+ }
78
+ return segments;
79
+ }
80
+ /**
81
+ * Apply edit decisions to segments.
82
+ * Returns paths to the retimed segment files.
83
+ */
84
+ function applyEdits(segments, decisions, workDir) {
85
+ const decisionsByBeat = new Map(decisions.decisions.map((d) => [d.beatId, d]));
86
+ return segments.map((seg) => {
87
+ const decision = decisionsByBeat.get(seg.sceneId);
88
+ if (!decision) {
89
+ debug(` ${seg.sceneId}: no edit needed`);
90
+ return seg;
91
+ }
92
+ const outputPath = resolve(workDir, `${seg.sceneId}-retimed.mp4`);
93
+ try {
94
+ switch (decision.editType) {
95
+ case "speed_video": {
96
+ const speed = decision.segmentEndMs / decision.targetDurationMs;
97
+ const filter = `setpts=${(1 / speed).toFixed(4)}*PTS`;
98
+ execSync(`ffmpeg -y -i "${seg.path}" -filter:v "${filter}" ` +
99
+ `-c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -an "${outputPath}"`, { encoding: "utf-8", timeout: 120_000, stdio: ["pipe", "pipe", "pipe"] });
100
+ console.log(` ${seg.sceneId}: speed_video ${speed.toFixed(2)}x`);
101
+ return { path: outputPath, sceneId: seg.sceneId };
102
+ }
103
+ case "slow_video": {
104
+ const speed = decision.segmentEndMs / decision.targetDurationMs;
105
+ const filter = `setpts=${(1 / speed).toFixed(4)}*PTS`;
106
+ execSync(`ffmpeg -y -i "${seg.path}" -filter:v "${filter}" ` +
107
+ `-c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -an "${outputPath}"`, { encoding: "utf-8", timeout: 120_000, stdio: ["pipe", "pipe", "pipe"] });
108
+ console.log(` ${seg.sceneId}: slow_video ${speed.toFixed(2)}x`);
109
+ return { path: outputPath, sceneId: seg.sceneId };
110
+ }
111
+ case "freeze_frame": {
112
+ const freezeDuration = decision.targetDurationMs / 1000;
113
+ // Extract last frame, create freeze, concatenate
114
+ const freezePath = resolve(workDir, `${seg.sceneId}-freeze.mp4`);
115
+ execSync(`ffmpeg -y -sseof -0.1 -i "${seg.path}" -vframes 1 -loop 1 -t ${freezeDuration} ` +
116
+ `-c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p "${freezePath}"`, { encoding: "utf-8", timeout: 60_000, stdio: ["pipe", "pipe", "pipe"] });
117
+ // Concatenate original + freeze
118
+ const listFile = resolve(workDir, `${seg.sceneId}-concat.txt`);
119
+ writeFileSync(listFile, `file '${seg.path}'\nfile '${freezePath}'\n`);
120
+ execSync(`ffmpeg -y -f concat -safe 0 -i "${listFile}" ` +
121
+ `-c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -an "${outputPath}"`, { encoding: "utf-8", timeout: 120_000, stdio: ["pipe", "pipe", "pipe"] });
122
+ console.log(` ${seg.sceneId}: freeze_frame +${decision.targetDurationMs}ms`);
123
+ return { path: outputPath, sceneId: seg.sceneId };
124
+ }
125
+ case "trim_silence": {
126
+ // Trim from the end of the segment to match target duration
127
+ const targetSec = decision.targetDurationMs / 1000;
128
+ execSync(`ffmpeg -y -i "${seg.path}" -t ${targetSec} ` +
129
+ `-c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -an "${outputPath}"`, { encoding: "utf-8", timeout: 120_000, stdio: ["pipe", "pipe", "pipe"] });
130
+ console.log(` ${seg.sceneId}: trim_silence to ${decision.targetDurationMs}ms`);
131
+ return { path: outputPath, sceneId: seg.sceneId };
132
+ }
133
+ case "pad_silence":
134
+ case "no_change":
135
+ debug(` ${seg.sceneId}: ${decision.editType} (no video change)`);
136
+ return seg;
137
+ default:
138
+ debug(` ${seg.sceneId}: unknown edit type ${decision.editType}`);
139
+ return seg;
140
+ }
141
+ }
142
+ catch (error) {
143
+ const execError = error;
144
+ console.warn(` Warning: Edit failed for ${seg.sceneId}, using original:`);
145
+ if (execError.stderr) {
146
+ debug(execError.stderr);
147
+ }
148
+ return seg;
149
+ }
150
+ });
151
+ }
152
+ /**
153
+ * Mux audio files into retimed video segments.
154
+ */
155
+ function muxAudioToSegments(segments, audioDir, workDir) {
156
+ return segments.map((seg) => {
157
+ // Find matching audio file
158
+ const wavPath = resolve(audioDir, `${seg.sceneId}.wav`);
159
+ const mp3Path = resolve(audioDir, `${seg.sceneId}.mp3`);
160
+ const audioPath = existsSync(wavPath) ? wavPath : existsSync(mp3Path) ? mp3Path : null;
161
+ if (!audioPath) {
162
+ debug(` ${seg.sceneId}: no audio file, keeping video-only`);
163
+ return seg;
164
+ }
165
+ const outputPath = resolve(workDir, `${seg.sceneId}-muxed.mp4`);
166
+ try {
167
+ execSync(`ffmpeg -y -i "${seg.path}" -i "${audioPath}" ` +
168
+ `-c:v copy -c:a aac -b:a 128k -shortest "${outputPath}"`, { encoding: "utf-8", timeout: 120_000, stdio: ["pipe", "pipe", "pipe"] });
169
+ return { path: outputPath, sceneId: seg.sceneId };
170
+ }
171
+ catch (error) {
172
+ const execError = error;
173
+ console.warn(` Warning: Audio mux failed for ${seg.sceneId}:`);
174
+ if (execError.stderr) {
175
+ debug(execError.stderr);
176
+ }
177
+ return seg;
178
+ }
179
+ });
180
+ }
181
+ /**
182
+ * Concatenate all segments into the final video.
183
+ */
184
+ function concatenateSegments(segments, outputPath, workDir) {
185
+ if (segments.length === 0)
186
+ return false;
187
+ // If only one segment, just copy it
188
+ if (segments.length === 1) {
189
+ try {
190
+ execSync(`ffmpeg -y -i "${segments[0].path}" -c copy "${outputPath}"`, { encoding: "utf-8", timeout: 120_000, stdio: ["pipe", "pipe", "pipe"] });
191
+ return true;
192
+ }
193
+ catch {
194
+ return false;
195
+ }
196
+ }
197
+ // Create concat file
198
+ const listFile = resolve(workDir, "concat-final.txt");
199
+ const lines = segments.map((s) => `file '${s.path}'`).join("\n");
200
+ writeFileSync(listFile, lines + "\n");
201
+ try {
202
+ execSync(`ffmpeg -y -f concat -safe 0 -i "${listFile}" ` +
203
+ `-c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -c:a aac -b:a 128k "${outputPath}"`, { encoding: "utf-8", timeout: 300_000, stdio: ["pipe", "pipe", "pipe"] });
204
+ return true;
205
+ }
206
+ catch (error) {
207
+ const execError = error;
208
+ console.error("Final concatenation failed:");
209
+ if (execError.stderr) {
210
+ console.error(execError.stderr);
211
+ }
212
+ return false;
213
+ }
214
+ }
215
+ //# sourceMappingURL=retiming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retiming.js","sourceRoot":"","sources":["../../src/lib/retiming.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Demofly Whisper integration — extract word-level timestamps from TTS audio.
3
+ *
4
+ * Shells out to the `whisper` CLI (same subprocess pattern as ffmpeg).
5
+ * Uses whisper-base model — TTS audio is clean, so a small model achieves
6
+ * near-perfect accuracy while keeping runtime under 5s per scene.
7
+ *
8
+ * Fallback: If Whisper isn't installed, returns duration-only data and
9
+ * prints a warning suggesting `pip install openai-whisper`.
10
+ */
11
+ export interface WordTimestamp {
12
+ word: string;
13
+ startMs: number;
14
+ endMs: number;
15
+ }
16
+ export interface PhraseTimestamp {
17
+ text: string;
18
+ startMs: number;
19
+ endMs: number;
20
+ beatId: string;
21
+ }
22
+ export interface SceneTimestamps {
23
+ sceneId: string;
24
+ audioFile: string;
25
+ durationMs: number;
26
+ words: WordTimestamp[];
27
+ phrases: PhraseTimestamp[];
28
+ }
29
+ export interface TimestampsData {
30
+ scenes: SceneTimestamps[];
31
+ }
32
+ /** Check if the whisper CLI is available. */
33
+ export declare function hasWhisper(): boolean;
34
+ /**
35
+ * Extract timestamps from all audio files in a demo's audio/ directory.
36
+ *
37
+ * If Whisper is available, produces word-level and phrase-level timestamps.
38
+ * If not, falls back to duration-only data (empty words/phrases arrays).
39
+ *
40
+ * @returns TimestampsData written to audio/timestamps.json
41
+ */
42
+ export declare function extractTimestamps(projectDir: string): TimestampsData;
43
+ //# sourceMappingURL=timestamps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timestamps.d.ts","sourceRoot":"","sources":["../../src/lib/timestamps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAaH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,6CAA6C;AAC7C,wBAAgB,UAAU,IAAI,OAAO,CAOpC;AAwHD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CA4EpE"}
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Demofly Whisper integration — extract word-level timestamps from TTS audio.
3
+ *
4
+ * Shells out to the `whisper` CLI (same subprocess pattern as ffmpeg).
5
+ * Uses whisper-base model — TTS audio is clean, so a small model achieves
6
+ * near-perfect accuracy while keeping runtime under 5s per scene.
7
+ *
8
+ * Fallback: If Whisper isn't installed, returns duration-only data and
9
+ * prints a warning suggesting `pip install openai-whisper`.
10
+ */
11
+ import { execSync } from "node:child_process";
12
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
13
+ import { resolve, basename, extname } from "node:path";
14
+ import { debug } from "./logger.js";
15
+ /** Check if the whisper CLI is available. */
16
+ export function hasWhisper() {
17
+ try {
18
+ execSync("whisper --help", { stdio: "pipe", timeout: 10_000 });
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ /**
26
+ * Get audio duration in milliseconds using ffprobe.
27
+ * Falls back to 0 if ffprobe isn't available.
28
+ */
29
+ function getAudioDurationMs(audioPath) {
30
+ try {
31
+ const output = execSync(`ffprobe -v error -show_entries format=duration -of csv=p=0 "${audioPath}"`, { encoding: "utf-8", timeout: 30_000, stdio: ["pipe", "pipe", "pipe"] });
32
+ return Math.round(parseFloat(output.trim()) * 1000);
33
+ }
34
+ catch {
35
+ return 0;
36
+ }
37
+ }
38
+ /**
39
+ * Run Whisper on a single audio file and extract word-level timestamps.
40
+ * Returns the raw word timestamps array.
41
+ */
42
+ function whisperTranscribe(audioPath, outputDir) {
43
+ const cmd = [
44
+ "whisper",
45
+ `"${audioPath}"`,
46
+ "--model", "base",
47
+ "--output_format", "json",
48
+ "--word_timestamps", "True",
49
+ "--output_dir", `"${outputDir}"`,
50
+ "--language", "en",
51
+ "--fp16", "False",
52
+ ].join(" ");
53
+ debug(`Whisper command: ${cmd}`);
54
+ try {
55
+ execSync(cmd, {
56
+ encoding: "utf-8",
57
+ timeout: 120_000,
58
+ stdio: ["pipe", "pipe", "pipe"],
59
+ });
60
+ }
61
+ catch (error) {
62
+ const execError = error;
63
+ console.warn(`Whisper transcription failed for ${audioPath}:`);
64
+ if (execError.stderr) {
65
+ console.warn(execError.stderr);
66
+ }
67
+ return [];
68
+ }
69
+ // Whisper outputs a JSON file with the same name as the input
70
+ const stem = basename(audioPath, extname(audioPath));
71
+ const jsonPath = resolve(outputDir, `${stem}.json`);
72
+ if (!existsSync(jsonPath)) {
73
+ console.warn(`Whisper output not found: ${jsonPath}`);
74
+ return [];
75
+ }
76
+ try {
77
+ const data = JSON.parse(readFileSync(jsonPath, "utf-8"));
78
+ const words = [];
79
+ // Whisper JSON format: { segments: [{ words: [{ word, start, end }] }] }
80
+ for (const segment of data.segments ?? []) {
81
+ for (const w of segment.words ?? []) {
82
+ words.push({
83
+ word: (w.word ?? "").trim(),
84
+ startMs: Math.round((w.start ?? 0) * 1000),
85
+ endMs: Math.round((w.end ?? 0) * 1000),
86
+ });
87
+ }
88
+ }
89
+ return words;
90
+ }
91
+ catch {
92
+ console.warn(`Failed to parse Whisper output: ${jsonPath}`);
93
+ return [];
94
+ }
95
+ }
96
+ /**
97
+ * Group word timestamps into phrases (sentences) based on punctuation.
98
+ * Each phrase gets a beatId placeholder that should be updated by the caller.
99
+ */
100
+ function groupIntoPhrases(words) {
101
+ if (words.length === 0)
102
+ return [];
103
+ const phrases = [];
104
+ let currentWords = [];
105
+ for (const word of words) {
106
+ currentWords.push(word);
107
+ // Split on sentence-ending punctuation
108
+ if (/[.!?]$/.test(word.word)) {
109
+ phrases.push({
110
+ text: currentWords.map((w) => w.word).join(" "),
111
+ startMs: currentWords[0].startMs,
112
+ endMs: word.endMs,
113
+ beatId: "", // To be filled by caller
114
+ });
115
+ currentWords = [];
116
+ }
117
+ }
118
+ // Remaining words form a final phrase
119
+ if (currentWords.length > 0) {
120
+ phrases.push({
121
+ text: currentWords.map((w) => w.word).join(" "),
122
+ startMs: currentWords[0].startMs,
123
+ endMs: currentWords[currentWords.length - 1].endMs,
124
+ beatId: "",
125
+ });
126
+ }
127
+ return phrases;
128
+ }
129
+ /**
130
+ * Extract timestamps from all audio files in a demo's audio/ directory.
131
+ *
132
+ * If Whisper is available, produces word-level and phrase-level timestamps.
133
+ * If not, falls back to duration-only data (empty words/phrases arrays).
134
+ *
135
+ * @returns TimestampsData written to audio/timestamps.json
136
+ */
137
+ export function extractTimestamps(projectDir) {
138
+ const audioDir = resolve(projectDir, "audio");
139
+ if (!existsSync(audioDir)) {
140
+ console.error(`No audio directory found at ${audioDir}`);
141
+ return { scenes: [] };
142
+ }
143
+ const audioFiles = readdirSync(audioDir).filter((f) => {
144
+ const ext = extname(f).toLowerCase();
145
+ return [".wav", ".mp3"].includes(ext);
146
+ });
147
+ if (audioFiles.length === 0) {
148
+ console.warn("No audio files found in audio/ directory.");
149
+ return { scenes: [] };
150
+ }
151
+ const whisperAvailable = hasWhisper();
152
+ if (!whisperAvailable) {
153
+ console.warn("Whisper not found. Using duration-only timestamps (less precise).\n" +
154
+ "For word-level timestamps, install: pip install openai-whisper");
155
+ }
156
+ // Create temp dir for Whisper output
157
+ const whisperTmpDir = resolve(audioDir, ".whisper-tmp");
158
+ const scenes = [];
159
+ for (const file of audioFiles.sort()) {
160
+ const sceneId = basename(file, extname(file));
161
+ const audioPath = resolve(audioDir, file);
162
+ const durationMs = getAudioDurationMs(audioPath);
163
+ console.log(` Timestamps: ${sceneId} (${durationMs}ms)`);
164
+ let words = [];
165
+ let phrases = [];
166
+ if (whisperAvailable) {
167
+ mkdirSync(whisperTmpDir, { recursive: true });
168
+ words = whisperTranscribe(audioPath, whisperTmpDir);
169
+ phrases = groupIntoPhrases(words);
170
+ console.log(` → ${words.length} words, ${phrases.length} phrases`);
171
+ }
172
+ else {
173
+ console.log(` → duration only (no Whisper)`);
174
+ }
175
+ scenes.push({
176
+ sceneId,
177
+ audioFile: `audio/${file}`,
178
+ durationMs,
179
+ words,
180
+ phrases,
181
+ });
182
+ }
183
+ // Clean up Whisper temp dir
184
+ if (existsSync(whisperTmpDir)) {
185
+ try {
186
+ execSync(`rm -rf "${whisperTmpDir}"`, { stdio: "pipe" });
187
+ }
188
+ catch {
189
+ // Non-critical cleanup failure
190
+ }
191
+ }
192
+ const result = { scenes };
193
+ // Write timestamps.json
194
+ const outputPath = resolve(audioDir, "timestamps.json");
195
+ writeFileSync(outputPath, JSON.stringify(result, null, 2), "utf-8");
196
+ console.log(`\n Written: ${outputPath}`);
197
+ return result;
198
+ }
199
+ //# sourceMappingURL=timestamps.js.map