tuna-agent 0.1.125 → 0.1.126
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.
|
@@ -174,30 +174,50 @@ export async function analyzeVideo(url, onProgress) {
|
|
|
174
174
|
const finalSlots = sceneSlots.slice(0, MAX_SCENES);
|
|
175
175
|
progress(`Đang cắt ${finalSlots.length} frames và phân tích...`);
|
|
176
176
|
console.log('[analyze_video] Building', finalSlots.length, 'scenes (segments:', segments.length, ', duration:', durationSec, 's)');
|
|
177
|
-
|
|
177
|
+
// Step 1: Extract all frames sequentially (ffmpeg can't run in parallel on same file efficiently)
|
|
178
|
+
const frameBuffers = [];
|
|
178
179
|
for (let i = 0; i < finalSlots.length; i++) {
|
|
179
180
|
const slot = finalSlots[i];
|
|
180
181
|
const midpoint = (slot.start + slot.end) / 2;
|
|
181
182
|
const framePath = path.join(framesDir, `scene-${String(i).padStart(3, '0')}.jpg`);
|
|
182
183
|
try {
|
|
183
|
-
progress(`Đang phân tích scene ${i + 1}/${finalSlots.length}...`);
|
|
184
184
|
await run(FFMPEG, ['-y', '-ss', String(midpoint), '-i', videoPath, '-vframes', '1', '-vf', 'scale=640:-1', '-q:v', '5', framePath, '-loglevel', 'error']);
|
|
185
185
|
const buf = await fs.readFile(framePath);
|
|
186
|
-
|
|
187
|
-
scenes.push({
|
|
188
|
-
scene_number: i + 1,
|
|
189
|
-
timestamp_start: Math.round(slot.start * 10) / 10,
|
|
190
|
-
timestamp_end: Math.round(slot.end * 10) / 10,
|
|
191
|
-
thumbnail_base64: buf.toString('base64'),
|
|
192
|
-
voiceover: slot.voiceover,
|
|
193
|
-
visual_description,
|
|
194
|
-
});
|
|
186
|
+
frameBuffers.push({ idx: i, buf, slot });
|
|
195
187
|
}
|
|
196
188
|
catch (err) {
|
|
197
189
|
const msg = err instanceof Error ? err.message : String(err);
|
|
198
190
|
console.warn('[analyze_video] Frame extract failed for scene', i, msg);
|
|
199
191
|
}
|
|
200
192
|
}
|
|
193
|
+
// Step 2: Vision describe all frames in parallel (batch of 5)
|
|
194
|
+
progress(`Đang phân tích ${frameBuffers.length} scenes song song...`);
|
|
195
|
+
const BATCH_SIZE = 5;
|
|
196
|
+
const sceneResults = [];
|
|
197
|
+
for (let b = 0; b < frameBuffers.length; b += BATCH_SIZE) {
|
|
198
|
+
const batch = frameBuffers.slice(b, b + BATCH_SIZE);
|
|
199
|
+
progress(`Đang phân tích scene ${b + 1}-${Math.min(b + BATCH_SIZE, frameBuffers.length)}/${frameBuffers.length}...`);
|
|
200
|
+
const results = await Promise.all(batch.map(async ({ idx, buf, slot }) => {
|
|
201
|
+
try {
|
|
202
|
+
const visual_description = await visionDescribe(buf.toString('base64'), slot.voiceover);
|
|
203
|
+
return {
|
|
204
|
+
scene_number: idx + 1,
|
|
205
|
+
timestamp_start: Math.round(slot.start * 10) / 10,
|
|
206
|
+
timestamp_end: Math.round(slot.end * 10) / 10,
|
|
207
|
+
thumbnail_base64: buf.toString('base64'),
|
|
208
|
+
voiceover: slot.voiceover,
|
|
209
|
+
visual_description,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
214
|
+
console.warn('[analyze_video] Vision describe failed for scene', idx, msg);
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}));
|
|
218
|
+
sceneResults.push(...results.filter((r) => r !== null));
|
|
219
|
+
}
|
|
220
|
+
const scenes = sceneResults.sort((a, b) => a.scene_number - b.scene_number);
|
|
201
221
|
return {
|
|
202
222
|
duration_sec: Math.round(durationSec),
|
|
203
223
|
language: transcript.language || 'unknown',
|