feat(video-edit): 新增双视频本地直拼模式(跳过生成)
This commit is contained in:
1 parent
f29e05a3a9
commit
9e91dfc38d
2 files changed
+111
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user