feat: 修复FFmpeg场景检测功能并添加调试工具
主要修复: - 重构场景检测算法,使用正确的FFmpeg命令 - 添加备用的简单场景检测方法 - 改进FFmpeg可用性检查,同时检查ffmpeg和ffprobe - 添加详细的FFmpeg状态信息获取功能 新增功能: - FFmpegDebugPanel调试面板组件 - test_scene_detection测试命令用于调试 - get_ffmpeg_status命令获取详细状态 - 项目详情页面添加调试工具选项卡 技术改进: - 更可靠的场景检测实现,支持降级到时间间隔方法 - 完善的错误处理和日志记录 - 用户友好的调试界面 - 实时测试和诊断工具 这个版本应该能够正确处理场景检测,即使在FFmpeg配置有问题的情况下也能提供备用方案。
This commit is contained in:
parent
954c8a6a1c
commit
dbcd98118c
|
|
@ -283,16 +283,22 @@ impl MaterialService {
|
|||
|
||||
// 2. 场景检测(如果是视频且启用了场景检测)
|
||||
if matches!(material.material_type, MaterialType::Video) && config.enable_scene_detection {
|
||||
println!("开始视频场景检测: {}", material.original_path);
|
||||
match Self::detect_video_scenes(&material.original_path, config.scene_detection_threshold) {
|
||||
Ok(scene_detection) => {
|
||||
println!("场景检测成功,发现 {} 个场景", scene_detection.scenes.len());
|
||||
material.set_scene_detection(scene_detection);
|
||||
repository.update(&material)?;
|
||||
}
|
||||
Err(e) => {
|
||||
// 场景检测失败不应该导致整个处理失败
|
||||
println!("场景检测失败: {}", e);
|
||||
eprintln!("场景检测失败: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("跳过场景检测 - 视频类型: {}, 启用检测: {}",
|
||||
matches!(material.material_type, MaterialType::Video),
|
||||
config.enable_scene_detection);
|
||||
}
|
||||
|
||||
// 3. 检查是否需要切分视频
|
||||
|
|
|
|||
|
|
@ -11,11 +11,50 @@ pub struct FFmpegService;
|
|||
impl FFmpegService {
|
||||
/// 检查 FFmpeg 是否可用
|
||||
pub fn is_available() -> bool {
|
||||
Command::new("ffprobe")
|
||||
let ffmpeg_available = Command::new("ffmpeg")
|
||||
.arg("-version")
|
||||
.output()
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
.unwrap_or(false);
|
||||
|
||||
let ffprobe_available = Command::new("ffprobe")
|
||||
.arg("-version")
|
||||
.output()
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false);
|
||||
|
||||
ffmpeg_available && ffprobe_available
|
||||
}
|
||||
|
||||
/// 获取详细的FFmpeg状态信息
|
||||
pub fn get_status_info() -> Result<String> {
|
||||
let mut info = String::new();
|
||||
|
||||
// 检查 ffmpeg
|
||||
match Command::new("ffmpeg").arg("-version").output() {
|
||||
Ok(output) if output.status.success() => {
|
||||
let version_str = String::from_utf8_lossy(&output.stdout);
|
||||
if let Some(first_line) = version_str.lines().next() {
|
||||
info.push_str(&format!("FFmpeg: {}\n", first_line));
|
||||
}
|
||||
}
|
||||
Ok(_) => info.push_str("FFmpeg: 命令执行失败\n"),
|
||||
Err(e) => info.push_str(&format!("FFmpeg: 未找到 ({})\n", e)),
|
||||
}
|
||||
|
||||
// 检查 ffprobe
|
||||
match Command::new("ffprobe").arg("-version").output() {
|
||||
Ok(output) if output.status.success() => {
|
||||
let version_str = String::from_utf8_lossy(&output.stdout);
|
||||
if let Some(first_line) = version_str.lines().next() {
|
||||
info.push_str(&format!("FFprobe: {}\n", first_line));
|
||||
}
|
||||
}
|
||||
Ok(_) => info.push_str("FFprobe: 命令执行失败\n"),
|
||||
Err(e) => info.push_str(&format!("FFprobe: 未找到 ({})\n", e)),
|
||||
}
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// 提取视频/音频元数据
|
||||
|
|
@ -193,34 +232,87 @@ impl FFmpegService {
|
|||
return Err(anyhow!("文件不存在: {}", file_path));
|
||||
}
|
||||
|
||||
let output = Command::new("ffprobe")
|
||||
// 首先尝试使用 ffmpeg 的 scene 滤镜
|
||||
match Self::detect_scenes_with_ffmpeg(file_path, threshold) {
|
||||
Ok(scenes) if !scenes.is_empty() => return Ok(scenes),
|
||||
Err(e) => {
|
||||
eprintln!("FFmpeg场景检测失败,使用备用方法: {}", e);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// 如果FFmpeg场景检测失败,使用简单的时间间隔方法
|
||||
Self::detect_scenes_simple(file_path, threshold)
|
||||
}
|
||||
|
||||
/// 使用FFmpeg进行场景检测
|
||||
fn detect_scenes_with_ffmpeg(file_path: &str, threshold: f64) -> Result<Vec<f64>> {
|
||||
let output = Command::new("ffmpeg")
|
||||
.args([
|
||||
"-f", "lavfi",
|
||||
"-i", &format!("movie={}:s=v:0[in];[in]select=gt(scene\\,{}),showinfo[out]", file_path, threshold),
|
||||
"-show_entries", "frame=pkt_pts_time",
|
||||
"-of", "csv=p=0",
|
||||
"-v", "quiet"
|
||||
"-i", file_path,
|
||||
"-vf", &format!("select='gt(scene,{})',showinfo", threshold),
|
||||
"-f", "null",
|
||||
"-"
|
||||
])
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.output()
|
||||
.map_err(|e| anyhow!("执行场景检测失败: {}", e))?;
|
||||
.map_err(|e| anyhow!("执行FFmpeg场景检测失败: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error_msg = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow!("场景检测失败: {}", error_msg));
|
||||
return Err(anyhow!("FFmpeg场景检测命令失败: {}", error_msg));
|
||||
}
|
||||
|
||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||
// 解析 stderr 中的 showinfo 输出
|
||||
let stderr_str = String::from_utf8_lossy(&output.stderr);
|
||||
let mut scene_times = Vec::new();
|
||||
|
||||
for line in output_str.lines() {
|
||||
if let Ok(time) = line.trim().parse::<f64>() {
|
||||
scene_times.push(time);
|
||||
|
||||
// 查找 showinfo 输出中的 pts_time 信息
|
||||
for line in stderr_str.lines() {
|
||||
if line.contains("showinfo") && line.contains("pts_time:") {
|
||||
if let Some(pts_start) = line.find("pts_time:") {
|
||||
let pts_part = &line[pts_start + 9..];
|
||||
if let Some(space_pos) = pts_part.find(' ') {
|
||||
let time_str = &pts_part[..space_pos];
|
||||
if let Ok(time) = time_str.parse::<f64>() {
|
||||
scene_times.push(time);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(scene_times)
|
||||
}
|
||||
|
||||
/// 简单的场景检测方法(备用)
|
||||
fn detect_scenes_simple(file_path: &str, threshold: f64) -> Result<Vec<f64>> {
|
||||
// 使用 ffprobe 获取视频时长,然后按固定间隔分割
|
||||
let metadata = Self::extract_metadata(file_path)?;
|
||||
|
||||
let duration = match metadata {
|
||||
MaterialMetadata::Video(video_meta) => video_meta.duration,
|
||||
_ => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
// 如果视频很短,不需要场景检测
|
||||
if duration < 60.0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// 按照阈值相关的间隔创建场景切点
|
||||
let interval = (60.0 / threshold).max(30.0).min(300.0); // 30秒到5分钟之间
|
||||
let mut scene_times = Vec::new();
|
||||
let mut current_time = interval;
|
||||
|
||||
while current_time < duration {
|
||||
scene_times.push(current_time);
|
||||
current_time += interval;
|
||||
}
|
||||
|
||||
Ok(scene_times)
|
||||
}
|
||||
|
||||
/// 切分视频
|
||||
pub fn split_video(
|
||||
input_path: &str,
|
||||
|
|
|
|||
|
|
@ -53,9 +53,11 @@ pub fn run() {
|
|||
commands::material_commands::get_file_info,
|
||||
commands::material_commands::check_ffmpeg_available,
|
||||
commands::material_commands::get_ffmpeg_version,
|
||||
commands::material_commands::get_ffmpeg_status,
|
||||
commands::material_commands::extract_file_metadata,
|
||||
commands::material_commands::detect_video_scenes,
|
||||
commands::material_commands::generate_video_thumbnail
|
||||
commands::material_commands::generate_video_thumbnail,
|
||||
commands::material_commands::test_scene_detection
|
||||
])
|
||||
.setup(|app| {
|
||||
// 初始化应用状态
|
||||
|
|
|
|||
|
|
@ -271,6 +271,12 @@ pub async fn get_ffmpeg_version() -> Result<String, String> {
|
|||
FFmpegService::get_version().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 获取 FFmpeg 状态信息命令
|
||||
#[command]
|
||||
pub async fn get_ffmpeg_status() -> Result<String, String> {
|
||||
FFmpegService::get_status_info().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 提取文件元数据命令
|
||||
#[command]
|
||||
pub async fn extract_file_metadata(file_path: String) -> Result<MaterialMetadata, String> {
|
||||
|
|
@ -298,3 +304,40 @@ pub async fn generate_video_thumbnail(
|
|||
FFmpegService::generate_thumbnail(&input_path, &output_path, timestamp, width, height)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 测试场景检测命令(用于调试)
|
||||
#[command]
|
||||
pub async fn test_scene_detection(file_path: String) -> Result<String, String> {
|
||||
// 首先检查FFmpeg状态
|
||||
let ffmpeg_status = FFmpegService::get_status_info().unwrap_or_else(|_| "无法获取FFmpeg状态".to_string());
|
||||
let mut result = format!("FFmpeg状态:\n{}\n", ffmpeg_status);
|
||||
|
||||
// 检查文件是否存在
|
||||
if !std::path::Path::new(&file_path).exists() {
|
||||
return Ok(format!("{}文件不存在: {}", result, file_path));
|
||||
}
|
||||
|
||||
result.push_str(&format!("测试文件: {}\n", file_path));
|
||||
|
||||
// 尝试提取元数据
|
||||
match FFmpegService::extract_metadata(&file_path) {
|
||||
Ok(metadata) => {
|
||||
result.push_str(&format!("元数据提取成功: {:?}\n", metadata));
|
||||
}
|
||||
Err(e) => {
|
||||
result.push_str(&format!("元数据提取失败: {}\n", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试场景检测
|
||||
match FFmpegService::detect_scenes(&file_path, 0.3) {
|
||||
Ok(scenes) => {
|
||||
result.push_str(&format!("场景检测成功,发现 {} 个场景切点: {:?}\n", scenes.len(), scenes));
|
||||
}
|
||||
Err(e) => {
|
||||
result.push_str(&format!("场景检测失败: {}\n", e));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Play, FileText, AlertCircle, CheckCircle } from 'lucide-react';
|
||||
import { useMaterialStore } from '../store/materialStore';
|
||||
|
||||
/**
|
||||
* FFmpeg调试面板组件
|
||||
* 用于测试和调试FFmpeg功能
|
||||
*/
|
||||
export const FFmpegDebugPanel: React.FC = () => {
|
||||
const {
|
||||
testSceneDetection,
|
||||
getFFmpegStatus,
|
||||
selectMaterialFiles
|
||||
} = useMaterialStore();
|
||||
|
||||
const [testFilePath, setTestFilePath] = useState('');
|
||||
const [testResult, setTestResult] = useState('');
|
||||
const [ffmpegStatus, setFFmpegStatus] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// 获取FFmpeg状态
|
||||
const handleGetFFmpegStatus = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const status = await getFFmpegStatus();
|
||||
setFFmpegStatus(status);
|
||||
} catch (error) {
|
||||
setFFmpegStatus(`获取状态失败: ${error}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 选择测试文件
|
||||
const handleSelectTestFile = async () => {
|
||||
try {
|
||||
const files = await selectMaterialFiles();
|
||||
if (files.length > 0) {
|
||||
setTestFilePath(files[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('选择文件失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 测试场景检测
|
||||
const handleTestSceneDetection = async () => {
|
||||
if (!testFilePath) {
|
||||
setTestResult('请先选择测试文件');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setTestResult('正在测试场景检测...');
|
||||
|
||||
try {
|
||||
const result = await testSceneDetection(testFilePath);
|
||||
setTestResult(result);
|
||||
} catch (error) {
|
||||
setTestResult(`测试失败: ${error}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<FileText className="w-5 h-5 mr-2" />
|
||||
FFmpeg 调试面板
|
||||
</h3>
|
||||
|
||||
{/* FFmpeg状态检查 */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="font-medium text-gray-900">FFmpeg 状态</h4>
|
||||
<button
|
||||
onClick={handleGetFFmpegStatus}
|
||||
disabled={isLoading}
|
||||
className="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
检查状态
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{ffmpegStatus && (
|
||||
<div className="bg-gray-50 rounded p-3 text-sm font-mono whitespace-pre-wrap">
|
||||
{ffmpegStatus}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 场景检测测试 */}
|
||||
<div className="mb-6">
|
||||
<h4 className="font-medium text-gray-900 mb-3">场景检测测试</h4>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
onClick={handleSelectTestFile}
|
||||
className="px-4 py-2 bg-gray-100 text-gray-700 rounded hover:bg-gray-200"
|
||||
>
|
||||
选择测试文件
|
||||
</button>
|
||||
{testFilePath && (
|
||||
<span className="text-sm text-gray-600 truncate max-w-md">
|
||||
{testFilePath}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleTestSceneDetection}
|
||||
disabled={!testFilePath || isLoading}
|
||||
className="flex items-center px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{isLoading ? '测试中...' : '测试场景检测'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 测试结果 */}
|
||||
{testResult && (
|
||||
<div className="mb-4">
|
||||
<h4 className="font-medium text-gray-900 mb-3">测试结果</h4>
|
||||
<div className="bg-gray-50 rounded p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto">
|
||||
{testResult}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 使用说明 */}
|
||||
<div className="bg-blue-50 rounded p-4">
|
||||
<h4 className="font-medium text-blue-900 mb-2 flex items-center">
|
||||
<AlertCircle className="w-4 h-4 mr-2" />
|
||||
使用说明
|
||||
</h4>
|
||||
<ul className="text-sm text-blue-800 space-y-1">
|
||||
<li>• 首先检查 FFmpeg 状态,确保 ffmpeg 和 ffprobe 都可用</li>
|
||||
<li>• 选择一个视频文件进行场景检测测试</li>
|
||||
<li>• 查看测试结果,包括元数据提取和场景检测信息</li>
|
||||
<li>• 如果场景检测失败,检查错误信息进行调试</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -8,6 +8,7 @@ import { MaterialImportResult } from '../types/material';
|
|||
import { LoadingSpinner } from '../components/LoadingSpinner';
|
||||
import { ErrorMessage } from '../components/ErrorMessage';
|
||||
import { MaterialImportDialog } from '../components/MaterialImportDialog';
|
||||
import { FFmpegDebugPanel } from '../components/FFmpegDebugPanel';
|
||||
|
||||
/**
|
||||
* 项目详情页面组件
|
||||
|
|
@ -26,6 +27,7 @@ export const ProjectDetails: React.FC = () => {
|
|||
} = useMaterialStore();
|
||||
const [project, setProject] = useState<Project | null>(null);
|
||||
const [showImportDialog, setShowImportDialog] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'materials' | 'debug'>('materials');
|
||||
|
||||
// 加载项目详情
|
||||
useEffect(() => {
|
||||
|
|
@ -183,54 +185,88 @@ export const ProjectDetails: React.FC = () => {
|
|||
{/* 素材管理 */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">素材管理</h2>
|
||||
{/* 选项卡导航 */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex space-x-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('materials')}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
|
||||
activeTab === 'materials'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
素材管理
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('debug')}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
|
||||
activeTab === 'debug'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
调试工具
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 素材导入区域 */}
|
||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center mb-6">
|
||||
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">导入素材文件</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
支持视频、音频、图片等多种格式,系统将自动分析并处理
|
||||
</p>
|
||||
<button
|
||||
onClick={handleMaterialImport}
|
||||
className="inline-flex items-center px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Upload className="w-5 h-5 mr-2" />
|
||||
选择文件导入
|
||||
</button>
|
||||
</div>
|
||||
{/* 选项卡内容 */}
|
||||
{activeTab === 'materials' && (
|
||||
<div>
|
||||
{/* 素材导入区域 */}
|
||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center mb-6">
|
||||
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">导入素材文件</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
支持视频、音频、图片等多种格式,系统将自动分析并处理
|
||||
</p>
|
||||
<button
|
||||
onClick={handleMaterialImport}
|
||||
className="inline-flex items-center px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Upload className="w-5 h-5 mr-2" />
|
||||
选择文件导入
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 素材列表 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-3">项目素材</h3>
|
||||
{materialsLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
) : materials.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{materials.map((material) => (
|
||||
<div key={material.id} className="border border-gray-200 rounded-lg p-4">
|
||||
<h4 className="font-medium text-gray-900 truncate">{material.name}</h4>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
类型: {material.material_type}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
状态: {material.processing_status}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
大小: {(material.file_size / 1024 / 1024).toFixed(2)} MB
|
||||
</p>
|
||||
{/* 素材列表 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-3">项目素材</h3>
|
||||
{materialsLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
))}
|
||||
) : materials.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{materials.map((material) => (
|
||||
<div key={material.id} className="border border-gray-200 rounded-lg p-4">
|
||||
<h4 className="font-medium text-gray-900 truncate">{material.name}</h4>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
类型: {material.material_type}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
状态: {material.processing_status}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
大小: {(material.file_size / 1024 / 1024).toFixed(2)} MB
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<p>暂无素材,请导入素材文件开始使用</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<p>暂无素材,请导入素材文件开始使用</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 调试工具选项卡 */}
|
||||
{activeTab === 'debug' && (
|
||||
<FFmpegDebugPanel />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -291,4 +291,26 @@ export const useMaterialStore = create<MaterialState>((set, get) => ({
|
|||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
// 测试场景检测
|
||||
testSceneDetection: async (filePath: string) => {
|
||||
try {
|
||||
const result = await invoke<string>('test_scene_detection', { filePath });
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('测试场景检测失败:', error);
|
||||
return `测试失败: ${error}`;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取FFmpeg状态
|
||||
getFFmpegStatus: async () => {
|
||||
try {
|
||||
const status = await invoke<string>('get_ffmpeg_status');
|
||||
return status;
|
||||
} catch (error) {
|
||||
console.error('获取FFmpeg状态失败:', error);
|
||||
return `获取状态失败: ${error}`;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
Loading…
Reference in New Issue