@lightcone-ai/daemon 0.15.76 → 0.15.77
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { mkdir, rm, writeFile, access } from 'node:fs/promises';
|
|
3
|
+
import { mkdir, rm, writeFile, access, stat as statAsync } from 'node:fs/promises';
|
|
4
4
|
import { constants as fsConstants } from 'node:fs';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
@@ -407,7 +407,18 @@ export async function composeVideoV2({
|
|
|
407
407
|
}
|
|
408
408
|
|
|
409
409
|
const totalDuration = await probeDurationSec(outPath);
|
|
410
|
-
|
|
410
|
+
|
|
411
|
+
// Stat the final file before returning so the caller can rely on size and
|
|
412
|
+
// so we can detect the (rare but observed) case where ffmpeg's `close`
|
|
413
|
+
// arrived but the kernel writeback wasn't complete. A 0-byte / tiny mp4
|
|
414
|
+
// here means the burn-subtitles pass produced nothing usable — fail loudly
|
|
415
|
+
// instead of letting a broken file flow into write_workspace_file / submit.
|
|
416
|
+
const finalStat = await statAsync(outPath);
|
|
417
|
+
const sizeBytes = Number(finalStat.size ?? 0);
|
|
418
|
+
if (!Number.isFinite(sizeBytes) || sizeBytes < 1024) {
|
|
419
|
+
throw new Error(`compose_video_v2 produced an invalid output: ${outPath} size=${sizeBytes} bytes`);
|
|
420
|
+
}
|
|
421
|
+
return { path: outPath, duration_ms: Math.round(totalDuration * 1000), size_bytes: sizeBytes };
|
|
411
422
|
} finally {
|
|
412
423
|
await rm(tmpDir, { recursive: true, force: true });
|
|
413
424
|
}
|
|
@@ -103,6 +103,7 @@ export async function runComposeVideoV2Tool({ segments, outro_paths, format, res
|
|
|
103
103
|
'compose_video_v2 completed.',
|
|
104
104
|
`path=${result.path}`,
|
|
105
105
|
`duration_ms=${result.duration_ms}`,
|
|
106
|
+
`size_bytes=${result.size_bytes ?? 'unknown'}`,
|
|
106
107
|
`segments=${segments.length}`,
|
|
107
108
|
`outro_clips=${(outro_paths ?? []).length}`,
|
|
108
109
|
];
|
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
import path, { extname } from 'node:path';
|
|
2
|
-
import { readFileSync, createReadStream } from 'node:fs';
|
|
2
|
+
import { readFileSync, createReadStream, statSync } from 'node:fs';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
|
|
5
|
+
// Wait for a file to stop growing before we read it. compose_video_v2 / ffmpeg
|
|
6
|
+
// occasionally finishes the wrapping tool-call promise a fraction earlier than
|
|
7
|
+
// the kernel finishes the last writeback for the output mp4 (observed: codex
|
|
8
|
+
// reports item.completed at T, file mtime is T+60s, and write_workspace_file
|
|
9
|
+
// reads a half-finished file in between). We stat → sleep → stat; if size or
|
|
10
|
+
// mtime moved, the file is still being written and we retry. Cheap (200-400ms
|
|
11
|
+
// for stable files; bounded retries for actively-growing ones).
|
|
12
|
+
async function waitForFileStable(localPath, { intervalMs = 300, attempts = 10 } = {}) {
|
|
13
|
+
let lastSize = -1;
|
|
14
|
+
let lastMtime = -1;
|
|
15
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
16
|
+
const st = statSync(localPath);
|
|
17
|
+
if (st.size === lastSize && st.mtimeMs === lastMtime) {
|
|
18
|
+
return { size: st.size };
|
|
19
|
+
}
|
|
20
|
+
lastSize = st.size;
|
|
21
|
+
lastMtime = st.mtimeMs;
|
|
22
|
+
await new Promise(r => setTimeout(r, intervalMs));
|
|
23
|
+
}
|
|
24
|
+
// Returned best-effort: caller can still proceed, but mismatch may surface
|
|
25
|
+
// at /storage/confirm size check.
|
|
26
|
+
return { size: lastSize };
|
|
27
|
+
}
|
|
28
|
+
|
|
5
29
|
export const WORKSPACE_BINARY_MIME = {
|
|
6
30
|
'.mp4': 'video/mp4',
|
|
7
31
|
'.png': 'image/png',
|
|
@@ -48,6 +72,11 @@ async function uploadBinaryFile({
|
|
|
48
72
|
presign, // async fn({ workspaceId, path, size, mime, sha256 }) → { uploadUrl, objectKey, alreadyExists }
|
|
49
73
|
confirmUpload, // async fn({ workspaceId, path, objectKey }) → void
|
|
50
74
|
}) {
|
|
75
|
+
// Wait until the source file is no longer being written. Without this we
|
|
76
|
+
// sometimes read a partial mp4 from a still-running ffmpeg, upload that
|
|
77
|
+
// partial buffer, and end up serving a truncated video downstream.
|
|
78
|
+
await waitForFileStable(localPath);
|
|
79
|
+
|
|
51
80
|
const buf = readFileSync(localPath);
|
|
52
81
|
const size = buf.length;
|
|
53
82
|
const sha256 = sha256ofBuffer(buf);
|