feat(video-edit): 新增双视频本地直拼模式(跳过生成)

This commit is contained in:
root committed 2026-02-17 18:35:41 +08:00
1 parent f29e05a3a9
commit 9e91dfc38d
2 files changed
+111

No files matched your search

+99
View File
@@ -21,6 +21,10 @@
const editLengthSelect = document.getElementById('editLengthSelect');
const editPromptInput = document.getElementById('editPromptInput');
const spliceBtn = document.getElementById('spliceBtn');
const pickMergeVideoBtn = document.getElementById('pickMergeVideoBtn');
const directMergeBtn = document.getElementById('directMergeBtn');
const mergeVideoA = document.getElementById('mergeVideoA');
const mergeVideoB = document.getElementById('mergeVideoB');
const promptInput = document.getElementById('promptInput');
const imageUrlInput = document.getElementById('imageUrlInput');
const parentPostInput = document.getElementById('parentPostInput');
@@ -73,6 +77,9 @@
let ffmpegLoading = false;
const DEFAULT_REASONING_EFFORT = 'low';
const EDIT_TIMELINE_MAX = 100000;
let mergeTargetVideoUrl = '';
let mergeTargetVideoName = '';
let cacheModalPickMode = 'edit';
function toast(message, type) {
if (typeof showToast === 'function') {
@@ -102,6 +109,15 @@
if (editFrameHash) editFrameHash.textContent = shortHash(lastFrameHash);
}
function updateMergeLabels() {
if (mergeVideoA) {
mergeVideoA.textContent = selectedVideoUrl ? shortHash(selectedVideoUrl) : '-';
}
if (mergeVideoB) {
mergeVideoB.textContent = mergeTargetVideoName || (mergeTargetVideoUrl ? shortHash(mergeTargetVideoUrl) : '-');
}
}
function getParentMemoryApi() {
return window.ParentPostMemory || null;
}
@@ -912,6 +928,7 @@
lockedTimestampMs = 0;
lastFrameHash = '';
setEditMeta();
updateMergeLabels();
}
function openEditPanel() {
@@ -931,6 +948,7 @@
editHint.classList.add('hidden');
editBody.classList.remove('hidden');
bindEditVideoSource(url);
updateMergeLabels();
}
function closeEditPanel() {
@@ -1014,6 +1032,14 @@
function useCachedVideo(url, name) {
const safeUrl = String(url || '').trim();
if (!safeUrl) return;
if (cacheModalPickMode === 'merge_target') {
mergeTargetVideoUrl = safeUrl;
mergeTargetVideoName = String(name || '').trim();
updateMergeLabels();
closeCacheVideoModal();
toast('已选择拼接视频B', 'success');
return;
}
selectedVideoItemId = `cache-${Date.now()}`;
selectedVideoUrl = safeUrl;
if (imageUrlInput) imageUrlInput.value = safeUrl;
@@ -1023,6 +1049,54 @@
openEditPanel();
}
async function directMergeTwoVideos() {
if (!selectedVideoUrl) {
toast('请先选中主视频A', 'warning');
return;
}
if (!mergeTargetVideoUrl) {
toast('请先选择拼接视频B', 'warning');
return;
}
if (editingBusy) {
toast('任务进行中', 'warning');
return;
}
editingBusy = true;
if (directMergeBtn) directMergeBtn.disabled = true;
setStatus('connecting', '本地拼接中');
try {
const ff = await ensureFfmpeg();
const a = await fetchArrayBuffer(selectedVideoUrl);
const b = await fetchArrayBuffer(mergeTargetVideoUrl);
await ffmpegWriteFile(ff, 'merge_a.mp4', a);
await ffmpegWriteFile(ff, 'merge_b.mp4', b);
await ffmpegExec(ff, ['-y', '-i', 'merge_a.mp4', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-ar', '48000', '-ac', '2', 'merge_a_norm.mp4']);
await ffmpegExec(ff, ['-y', '-i', 'merge_b.mp4', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-ar', '48000', '-ac', '2', 'merge_b_norm.mp4']);
await ffmpegWriteFile(ff, 'merge_list.txt', new TextEncoder().encode("file 'merge_a_norm.mp4'\nfile 'merge_b_norm.mp4'\n"));
await ffmpegExec(ff, ['-y', '-f', 'concat', '-safe', '0', '-i', 'merge_list.txt', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-ar', '48000', '-ac', '2', 'merge_out.mp4']);
const out = await ffmpegReadFile(ff, 'merge_out.mp4');
const outBlob = new Blob([new Uint8Array(out)], { type: 'video/mp4' });
const outUrl = URL.createObjectURL(outBlob);
const item = getSelectedVideoItem();
if (item) {
renderVideoFromUrl({ previewItem: item }, outUrl);
}
bindEditVideoSource(outUrl);
mergeTargetVideoUrl = '';
mergeTargetVideoName = '';
updateMergeLabels();
setStatus('connected', '拼接完成');
toast('已完成两段视频拼接', 'success');
} catch (e) {
setStatus('error', '拼接失败');
toast(`拼接失败: ${String(e && e.message ? e.message : e)}`, 'error');
} finally {
editingBusy = false;
if (directMergeBtn) directMergeBtn.disabled = false;
}
}
function updateTimelineByVideoTime() {
if (!editVideo || !editTimeline) return;
const duration = Number(editVideo.duration || 0);
@@ -1591,10 +1665,34 @@
runSplice();
});
}
if (directMergeBtn) {
directMergeBtn.addEventListener('click', () => {
directMergeTwoVideos();
});
}
if (pickCachedVideoBtn) {
pickCachedVideoBtn.addEventListener('click', async () => {
try {
cacheModalPickMode = 'edit';
openCacheVideoModal();
if (cacheVideoList) {
cacheVideoList.innerHTML = '<div class="video-empty">正在读取缓存视频...</div>';
}
const items = await loadCachedVideos();
renderCachedVideoList(items);
} catch (e) {
if (cacheVideoList) {
cacheVideoList.innerHTML = '<div class="video-empty">读取失败,请稍后重试</div>';
}
toast('读取缓存视频失败', 'error');
}
});
}
if (pickMergeVideoBtn) {
pickMergeVideoBtn.addEventListener('click', async () => {
try {
cacheModalPickMode = 'merge_target';
openCacheVideoModal();
if (cacheVideoList) {
cacheVideoList.innerHTML = '<div class="video-empty">正在读取缓存视频...</div>';
@@ -1647,6 +1745,7 @@
if (enterEditBtn) {
enterEditBtn.disabled = !selectedVideoUrl;
}
updateMergeLabels();
if (target.classList.contains('video-edit')) {
event.preventDefault();
openEditPanel();
+12
View File
@@ -218,11 +218,23 @@
</select>
</div>
</div>
<div class="meta-grid edit-meta-grid">
<div class="meta-item">
<div class="meta-label">主视频A</div>
<div id="mergeVideoA" class="meta-value">-</div>
</div>
<div class="meta-item">
<div class="meta-label">拼接视频B</div>
<div id="mergeVideoB" class="meta-value">-</div>
</div>
</div>
<div class="settings-block prompt-block edit-prompt-block">
<label class="field-label" for="editPromptInput">拼接提示词</label>
<textarea id="editPromptInput" class="geist-input video-textarea" placeholder="描述后续要拼接的画面内容"></textarea>
</div>
<div class="preview-actions">
<button id="pickMergeVideoBtn" class="geist-button-outline text-xs px-3">选择视频B</button>
<button id="directMergeBtn" class="geist-button-outline text-xs px-3">直接拼接(跳过生成)</button>
<button id="spliceBtn" class="geist-button text-xs px-3">拼接视频</button>
</div>
</div>