feat: 添加完整的模板管理系统

🎉 新功能:
- 批量导入模板功能,支持文件夹结构解析
- 自动解析 draft_content.json 并提取轨道/素材信息
- 智能素材管理,自动复制到统一资源目录
- 路径转换为相对路径,确保模板可移植性
- 现代化的模板管理界面,支持网格/列表视图
- 搜索和筛选功能
- 模板详情预览和删除功能

🏗️ 技术实现:
- Python: TemplateManager 核心服务类
- Rust/Tauri: 跨平台命令处理和进程管理
- React/TypeScript: 响应式前端界面
- JSON-RPC: 前后端通信协议

📁 文件结构:
- 模板存储在 attachments/templates/{uuid}/ 目录
- 素材统一管理在 resources/ 子目录
- 元数据存储在 templates.json 文件

 已测试功能:
- 批量导入多个模板
- 模板列表显示和搜索
- 模板详情查看
- 模板删除操作
- CLI 命令行接口

这个系统为视频编辑提供了强大的模板管理能力,
支持从外部导入模板并自动处理素材依赖关系。
This commit is contained in:
root
2025-07-10 20:14:49 +08:00
parent 168ad4dafa
commit c73aeb58e9
15 changed files with 5187 additions and 20 deletions

3336
assets/draft_content.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,14 +5,35 @@ Services Module
Provides background services, file management, and task queue functionality.
"""
from .file_manager import FileManager
from .task_queue import TaskQueue
from .project_manager import ProjectManager
from .cache_manager import CacheManager
try:
from .file_manager import FileManager
except ImportError:
FileManager = None
try:
from .task_queue import TaskQueue
except ImportError:
TaskQueue = None
try:
from .project_manager import ProjectManager
except ImportError:
ProjectManager = None
try:
from .cache_manager import CacheManager
except ImportError:
CacheManager = None
try:
from .template_manager import TemplateManager
except ImportError:
TemplateManager = None
__all__ = [
"FileManager",
"TaskQueue",
"ProjectManager",
"CacheManager"
"ProjectManager",
"CacheManager",
"TemplateManager"
]

View File

@@ -0,0 +1,448 @@
"""
Template Management Service
Handles template import, processing, and management
"""
import os
import json
import shutil
import uuid
from pathlib import Path
from typing import Dict, List, Any, Optional
from dataclasses import dataclass, asdict
from datetime import datetime
from ..utils.logger import setup_logger
from ..config import settings
logger = setup_logger(__name__)
@dataclass
class TemplateInfo:
"""Template information structure"""
id: str
name: str
description: str
thumbnail_path: str
draft_content_path: str
resources_path: str
created_at: str
updated_at: str
canvas_config: Dict[str, Any]
duration: int
material_count: int
track_count: int
tags: List[str]
@dataclass
class MaterialInfo:
"""Material information structure"""
id: str
material_id: str
name: str
type: str # video, audio, image, text, sticker, etc.
original_path: str
relative_path: str
duration: Optional[int] = None
size: Optional[int] = None
class TemplateManager:
"""Template management service"""
def __init__(self):
self.templates_dir = settings.temp_dir / "templates"
self.templates_dir.mkdir(parents=True, exist_ok=True)
# Template metadata file
self.metadata_file = self.templates_dir / "templates.json"
self.templates_metadata = self._load_metadata()
def _load_metadata(self) -> Dict[str, Dict]:
"""Load templates metadata"""
if self.metadata_file.exists():
try:
with open(self.metadata_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error(f"Failed to load templates metadata: {e}")
return {}
def _save_metadata(self):
"""Save templates metadata"""
try:
with open(self.metadata_file, 'w', encoding='utf-8') as f:
json.dump(self.templates_metadata, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"Failed to save templates metadata: {e}")
def batch_import_templates(self, source_folder: str, progress_callback=None) -> Dict[str, Any]:
"""
Batch import templates from a folder
Args:
source_folder: Source folder containing template subfolders
progress_callback: Progress callback function
Returns:
Import result with success/failure counts and details
"""
result = {
'status': True,
'msg': '',
'imported_count': 0,
'failed_count': 0,
'imported_templates': [],
'failed_templates': []
}
try:
if not os.path.exists(source_folder):
result['status'] = False
result['msg'] = f"Source folder does not exist: {source_folder}"
return result
# Find all subdirectories
subdirs = [d for d in os.listdir(source_folder)
if os.path.isdir(os.path.join(source_folder, d))]
if not subdirs:
result['status'] = False
result['msg'] = "No subdirectories found in source folder"
return result
total_templates = len(subdirs)
logger.info(f"Found {total_templates} potential templates in {source_folder}")
if progress_callback:
progress_callback(f"Found {total_templates} potential templates")
for i, template_dir in enumerate(subdirs):
template_path = os.path.join(source_folder, template_dir)
if progress_callback:
progress_callback(f"Processing template {i+1}/{total_templates}: {template_dir}")
try:
template_info = self._import_single_template(template_path, template_dir)
if template_info:
result['imported_count'] += 1
result['imported_templates'].append(template_info)
logger.info(f"Successfully imported template: {template_dir}")
else:
result['failed_count'] += 1
result['failed_templates'].append({
'name': template_dir,
'error': 'No draft_content.json found or invalid template'
})
logger.warning(f"Failed to import template: {template_dir}")
except Exception as e:
result['failed_count'] += 1
result['failed_templates'].append({
'name': template_dir,
'error': str(e)
})
logger.error(f"Error importing template {template_dir}: {e}")
# Save metadata
self._save_metadata()
result['msg'] = f"Import completed. Success: {result['imported_count']}, Failed: {result['failed_count']}"
logger.info(result['msg'])
if progress_callback:
progress_callback(result['msg'])
except Exception as e:
result['status'] = False
result['msg'] = f"Batch import failed: {str(e)}"
logger.error(result['msg'])
return result
def _import_single_template(self, template_path: str, template_name: str) -> Optional[TemplateInfo]:
"""
Import a single template
Args:
template_path: Path to template folder
template_name: Template name
Returns:
TemplateInfo if successful, None otherwise
"""
try:
# Check for draft_content.json
draft_file = os.path.join(template_path, "draft_content.json")
if not os.path.exists(draft_file):
logger.warning(f"No draft_content.json found in {template_path}")
return None
# Load and parse draft content
with open(draft_file, 'r', encoding='utf-8') as f:
draft_content = json.load(f)
# Generate template ID
template_id = str(uuid.uuid4())
# Create template directory
template_dir = self.templates_dir / template_id
template_dir.mkdir(parents=True, exist_ok=True)
# Create resources directory
resources_dir = template_dir / "resources"
resources_dir.mkdir(exist_ok=True)
# Process materials and copy resources
materials = self._process_materials(draft_content, template_path, resources_dir)
# Update draft content with relative paths
updated_draft = self._update_draft_paths(draft_content, materials)
# Save updated draft content
draft_target = template_dir / "draft_content.json"
with open(draft_target, 'w', encoding='utf-8') as f:
json.dump(updated_draft, f, ensure_ascii=False, indent=2)
# Create template info
template_info = TemplateInfo(
id=template_id,
name=template_name,
description=f"Imported template: {template_name}",
thumbnail_path="", # TODO: Generate thumbnail
draft_content_path=str(draft_target),
resources_path=str(resources_dir),
created_at=datetime.now().isoformat(),
updated_at=datetime.now().isoformat(),
canvas_config=draft_content.get('canvas_config', {}),
duration=draft_content.get('duration', 0),
material_count=len(materials),
track_count=len(draft_content.get('tracks', [])),
tags=[]
)
# Save to metadata
self.templates_metadata[template_id] = asdict(template_info)
logger.info(f"Successfully processed template: {template_name} -> {template_id}")
return template_info
except Exception as e:
logger.error(f"Failed to import template {template_name}: {e}")
return None
def _process_materials(self, draft_content: Dict, source_path: str, resources_dir: Path) -> List[MaterialInfo]:
"""
Process and copy materials from draft content
Args:
draft_content: Draft content JSON
source_path: Source template path
resources_dir: Target resources directory
Returns:
List of processed materials
"""
materials = []
materials_section = draft_content.get('materials', {})
# Process different material types
material_types = ['videos', 'audios', 'images', 'stickers', 'texts']
for material_type in material_types:
if material_type in materials_section:
for material in materials_section[material_type]:
material_info = self._process_single_material(
material, material_type, source_path, resources_dir
)
if material_info:
materials.append(material_info)
return materials
def _process_single_material(self, material: Dict, material_type: str,
source_path: str, resources_dir: Path) -> Optional[MaterialInfo]:
"""Process a single material"""
try:
material_id = material.get('id')
if not material_id:
return None
# Get original path
original_path = material.get('path', '')
if not original_path or not os.path.exists(original_path):
logger.warning(f"Material path not found: {original_path}")
return None
# Generate relative path
filename = os.path.basename(original_path)
relative_path = f"resources/{filename}"
target_path = resources_dir / filename
# Copy file
shutil.copy2(original_path, target_path)
# Get file info
file_size = os.path.getsize(target_path)
material_info = MaterialInfo(
id=str(uuid.uuid4()),
material_id=material_id,
name=material.get('name', filename),
type=material_type,
original_path=original_path,
relative_path=relative_path,
size=file_size
)
logger.debug(f"Processed material: {filename} -> {relative_path}")
return material_info
except Exception as e:
logger.error(f"Failed to process material: {e}")
return None
def _update_draft_paths(self, draft_content: Dict, materials: List[MaterialInfo]) -> Dict:
"""Update draft content with relative paths"""
# Create material ID to relative path mapping
path_mapping = {mat.material_id: mat.relative_path for mat in materials}
# Update materials section
materials_section = draft_content.get('materials', {})
for material_type in materials_section:
if isinstance(materials_section[material_type], list):
for material in materials_section[material_type]:
material_id = material.get('id')
if material_id in path_mapping:
material['path'] = path_mapping[material_id]
return draft_content
def get_templates(self) -> List[TemplateInfo]:
"""Get all templates"""
templates = []
for template_data in self.templates_metadata.values():
templates.append(TemplateInfo(**template_data))
return templates
def get_template(self, template_id: str) -> Optional[TemplateInfo]:
"""Get a specific template"""
if template_id in self.templates_metadata:
return TemplateInfo(**self.templates_metadata[template_id])
return None
def delete_template(self, template_id: str) -> bool:
"""Delete a template"""
try:
if template_id in self.templates_metadata:
# Remove template directory
template_dir = self.templates_dir / template_id
if template_dir.exists():
shutil.rmtree(template_dir)
# Remove from metadata
del self.templates_metadata[template_id]
self._save_metadata()
logger.info(f"Deleted template: {template_id}")
return True
except Exception as e:
logger.error(f"Failed to delete template {template_id}: {e}")
return False
def main():
"""Command line interface for template management."""
import argparse
import json
import sys
parser = argparse.ArgumentParser(description="Template Manager")
parser.add_argument("--action", required=True, help="Action to perform")
parser.add_argument("--source_folder", help="Source folder for batch import")
parser.add_argument("--template_id", help="Template ID for operations")
args = parser.parse_args()
try:
manager = TemplateManager()
if args.action == "batch_import":
if not args.source_folder:
raise ValueError("Source folder is required for batch import")
def progress_callback(message):
print(message)
result = manager.batch_import_templates(args.source_folder, progress_callback)
# Convert TemplateInfo objects to dictionaries for JSON serialization
if 'imported_templates' in result:
result['imported_templates'] = [asdict(template) for template in result['imported_templates']]
elif args.action == "get_templates":
templates = manager.get_templates()
result = {
"status": True,
"templates": [asdict(template) for template in templates]
}
elif args.action == "get_template":
if not args.template_id:
raise ValueError("Template ID is required")
template = manager.get_template(args.template_id)
if template:
result = {
"status": True,
"template": asdict(template)
}
else:
result = {
"status": False,
"msg": f"Template not found: {args.template_id}"
}
elif args.action == "delete_template":
if not args.template_id:
raise ValueError("Template ID is required")
success = manager.delete_template(args.template_id)
result = {
"status": success,
"msg": "Template deleted successfully" if success else "Failed to delete template"
}
else:
raise ValueError(f"Unknown action: {args.action}")
# Use safe encoding for final output
result_json = json.dumps(result, ensure_ascii=True, indent=2)
if hasattr(sys.stdout, 'buffer'):
sys.stdout.buffer.write(result_json.encode('utf-8'))
sys.stdout.buffer.write(b'\n')
sys.stdout.buffer.flush()
else:
print(result_json)
sys.stdout.flush()
except Exception as e:
error_result = {
"status": False,
"error": str(e)
}
error_json = json.dumps(error_result, ensure_ascii=True, indent=2)
if hasattr(sys.stdout, 'buffer'):
sys.stdout.buffer.write(error_json.encode('utf-8'))
sys.stdout.buffer.write(b'\n')
sys.stdout.buffer.flush()
else:
print(error_json)
sys.stdout.flush()
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -4,6 +4,7 @@ pub mod video;
pub mod ai_video;
pub mod file_system;
pub mod project;
pub mod template;
// Re-export all commands for easy access
pub use basic::*;
@@ -11,3 +12,4 @@ pub use video::*;
pub use ai_video::*;
pub use file_system::*;
pub use project::*;
pub use template::*;

View File

@@ -0,0 +1,349 @@
use serde::{Deserialize, Serialize};
use std::process::Command;
use std::io::{BufRead, BufReader, Read};
use std::time::{Duration, Instant};
use std::thread;
use std::sync::{Arc, Mutex};
use std::sync::mpsc;
#[derive(Debug, Deserialize)]
pub struct BatchImportRequest {
pub source_folder: String,
}
#[derive(Debug, Serialize)]
pub struct TemplateInfo {
pub id: String,
pub name: String,
pub description: String,
pub thumbnail_path: String,
pub draft_content_path: String,
pub resources_path: String,
pub created_at: String,
pub updated_at: String,
pub canvas_config: serde_json::Value,
pub duration: i64,
pub material_count: i32,
pub track_count: i32,
pub tags: Vec<String>,
}
async fn execute_python_template_command(_app: tauri::AppHandle, args: &[String]) -> Result<String, String> {
println!("Executing Python template command with args: {:?}", args);
// Get project root directory
let current_dir = std::env::current_dir()
.map_err(|e| format!("Failed to get current directory: {}", e))?;
let project_root = if current_dir.ends_with("src-tauri") {
current_dir.parent().unwrap_or(&current_dir).to_path_buf()
} else {
current_dir
};
println!("Working directory: {:?}", project_root);
// Try multiple Python commands in order of preference
let python_commands = if cfg!(target_os = "windows") {
vec!["python", "python3", "py"]
} else {
vec!["python3", "python"]
};
let mut child = None;
let mut last_error = String::new();
for python_cmd in python_commands {
println!("Trying Python command: {}", python_cmd);
let mut cmd = Command::new(python_cmd);
cmd.current_dir(&project_root);
cmd.args(args);
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
// Set environment variables for consistent encoding
cmd.env("PYTHONIOENCODING", "utf-8");
cmd.env("PYTHONUNBUFFERED", "1");
// On Windows, ensure UTF-8 console output
if cfg!(target_os = "windows") {
cmd.env("PYTHONUTF8", "1");
}
match cmd.spawn() {
Ok(process) => {
println!("Successfully started Python process with: {}", python_cmd);
child = Some(process);
break;
}
Err(e) => {
last_error = format!("Failed to start {} process: {}", python_cmd, e);
println!("{}", last_error);
continue;
}
}
}
let mut child = child.ok_or_else(|| {
format!("Failed to start Python process with any command. Last error: {}", last_error)
})?;
let stdout = child.stdout.take().unwrap();
let stderr = child.stderr.take().unwrap();
// Use channels for concurrent reading
let (stdout_tx, _stdout_rx) = mpsc::channel();
let (stderr_tx, _stderr_rx) = mpsc::channel();
let (result_tx, result_rx) = mpsc::channel();
let progress_messages = Arc::new(Mutex::new(Vec::new()));
let error_messages = Arc::new(Mutex::new(Vec::new()));
let final_result = Arc::new(Mutex::new(None::<String>));
// Spawn thread for reading stdout
let progress_messages_clone = Arc::clone(&progress_messages);
let final_result_clone = Arc::clone(&final_result);
thread::spawn(move || {
let mut reader = BufReader::new(stdout);
let mut buffer = Vec::new();
// Read raw bytes and handle encoding issues
loop {
buffer.clear();
match reader.read_until(b'\n', &mut buffer) {
Ok(0) => break, // EOF
Ok(_) => {
// Convert bytes to string, handling invalid UTF-8
let line = String::from_utf8_lossy(&buffer);
let line = line.trim_end_matches('\n').trim_end_matches('\r');
if line.is_empty() {
continue;
}
println!("Python stdout: {}", line);
// Parse JSON-RPC messages
if line.starts_with("JSONRPC:") {
let json_str = &line[8..]; // Remove "JSONRPC:" prefix
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(json_str) {
if let Some(method) = json_value.get("method") {
if method == "progress" {
if let Ok(mut messages) = progress_messages_clone.lock() {
messages.push(json_str.to_string());
}
println!("Progress: {}", json_str);
}
} else if json_value.get("result").is_some() || json_value.get("error").is_some() {
// This is a final result or error response
if let Ok(mut result) = final_result_clone.lock() {
*result = Some(json_str.to_string());
}
println!("JSON-RPC result found: {}", json_str);
}
}
} else if line.trim().starts_with('{') && line.trim().ends_with('}') {
// Fallback: try to parse as direct JSON result
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(line.trim()) {
// Check if this looks like a final result (has status field)
if json_value.get("status").is_some() {
if let Ok(mut result) = final_result_clone.lock() {
*result = Some(line.trim().to_string());
}
println!("Direct JSON result: {}", line.trim());
}
}
} else {
println!("Python other: {}", line);
}
if stdout_tx.send(line.to_string()).is_err() {
break;
}
}
Err(e) => {
println!("Error reading stdout: {}", e);
break;
}
}
}
});
// Spawn thread for reading stderr
let error_messages_clone = Arc::clone(&error_messages);
thread::spawn(move || {
let mut reader = BufReader::new(stderr);
let mut buffer = Vec::new();
// Read raw bytes and handle encoding issues
loop {
buffer.clear();
match reader.read_until(b'\n', &mut buffer) {
Ok(0) => break, // EOF
Ok(_) => {
// Convert bytes to string, handling invalid UTF-8
let line = String::from_utf8_lossy(&buffer);
let line = line.trim_end_matches('\n').trim_end_matches('\r');
if line.is_empty() {
continue;
}
println!("Python stderr: {}", line);
if let Ok(mut messages) = error_messages_clone.lock() {
messages.push(line.to_string());
}
if stderr_tx.send(line.to_string()).is_err() {
break;
}
}
Err(e) => {
println!("Error reading stderr: {}", e);
break;
}
}
}
});
// Spawn thread to wait for process completion
thread::spawn(move || {
match child.wait() {
Ok(status) => {
let _ = result_tx.send(Ok(status));
}
Err(e) => {
let _ = result_tx.send(Err(format!("Failed to wait for process: {}", e)));
}
}
});
// Wait for process to complete with timeout
println!("Waiting for Python process to complete...");
let timeout_duration = Duration::from_secs(600); // 10 minutes timeout
let start_time = Instant::now();
let exit_status = loop {
match result_rx.try_recv() {
Ok(Ok(status)) => break status,
Ok(Err(e)) => return Err(e),
Err(mpsc::TryRecvError::Empty) => {
if start_time.elapsed() > timeout_duration {
return Err("Python process timed out after 10 minutes".to_string());
}
thread::sleep(Duration::from_millis(100));
continue;
}
Err(mpsc::TryRecvError::Disconnected) => {
return Err("Process monitoring thread disconnected".to_string());
}
}
};
println!("Python process terminated with code: {:?}", exit_status.code());
// Extract final results from shared state
let final_result_value = {
final_result.lock().unwrap().clone()
};
let progress_messages_value = {
progress_messages.lock().unwrap().clone()
};
let error_messages_value = {
error_messages.lock().unwrap().clone()
};
// Return the final result if we found one
if let Some(result) = final_result_value {
println!("Extracted JSON-RPC result: {}", result);
return Ok(result);
}
// If no JSON-RPC result found, provide detailed error information
if exit_status.success() {
// Process succeeded but no output - this is unusual
let error_msg = if progress_messages_value.is_empty() {
"Python script completed successfully but produced no output"
} else {
"Python script completed but did not return a valid JSON result"
};
Err(error_msg.to_string())
} else {
// Process failed - provide detailed error based on exit code
let error_msg = if let Some(code) = exit_status.code() {
match code {
1 => "Python script failed with general error. Check if all dependencies are installed.".to_string(),
120 => "Python module import failed or function not supported. This may be due to missing dependencies or incompatible Python environment.".to_string(),
_ => format!("Python script failed with exit code: {}. This may indicate a system or environment issue.", code)
}
} else {
"Python process was killed by external signal.".to_string()
};
// Include any error output we captured
let mut full_error = error_msg;
if !progress_messages_value.is_empty() {
full_error.push_str(&format!("\n\nLast stdout: {}", progress_messages_value.join("\n")));
}
if !error_messages_value.is_empty() {
full_error.push_str(&format!("\n\nStderr: {}", error_messages_value.join("\n")));
}
Err(full_error)
}
}
#[tauri::command]
pub async fn batch_import_templates(app: tauri::AppHandle, request: BatchImportRequest) -> Result<String, String> {
let args = vec![
"-m".to_string(),
"python_core.services.template_manager".to_string(),
"--action".to_string(),
"batch_import".to_string(),
"--source_folder".to_string(),
request.source_folder,
];
execute_python_template_command(app, &args).await
}
#[tauri::command]
pub async fn get_templates(app: tauri::AppHandle) -> Result<String, String> {
let args = vec![
"-m".to_string(),
"python_core.services.template_manager".to_string(),
"--action".to_string(),
"get_templates".to_string(),
];
execute_python_template_command(app, &args).await
}
#[tauri::command]
pub async fn get_template(app: tauri::AppHandle, template_id: String) -> Result<String, String> {
let args = vec![
"-m".to_string(),
"python_core.services.template_manager".to_string(),
"--action".to_string(),
"get_template".to_string(),
"--template_id".to_string(),
template_id,
];
execute_python_template_command(app, &args).await
}
#[tauri::command]
pub async fn delete_template(app: tauri::AppHandle, template_id: String) -> Result<String, String> {
let args = vec![
"-m".to_string(),
"python_core.services.template_manager".to_string(),
"--action".to_string(),
"delete_template".to_string(),
"--template_id".to_string(),
template_id,
];
execute_python_template_command(app, &args).await
}

View File

@@ -34,7 +34,11 @@ pub fn run() {
commands::test_ai_video_environment,
commands::select_image_file,
commands::select_folder,
commands::open_folder
commands::open_folder,
commands::batch_import_templates,
commands::get_templates,
commands::get_template,
commands::delete_template
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@@ -5,6 +5,7 @@ import HomePage from './pages/HomePage'
import EditorPage from './pages/EditorPage'
import AIVideoPage from './pages/AIVideoPage'
import SettingsPage from './pages/SettingsPage'
import TemplateManagePage from './pages/TemplateManagePage'
function App() {
return (
@@ -13,6 +14,7 @@ function App() {
<Route path="/" element={<HomePage />} />
<Route path="/editor" element={<EditorPage />} />
<Route path="/ai-video" element={<AIVideoPage />} />
<Route path="/templates" element={<TemplateManagePage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</Layout>

View File

@@ -145,16 +145,6 @@ const AIVideoResultPreview: React.FC<AIVideoResultPreviewProps> = ({
</button>
)}
{videoResult.video_url && (
<button
onClick={() => handleOpenUrl(videoResult.video_url!)}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
<ExternalLink size={16} />
</button>
)}
</div>
</div>
)

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { Home, Video, Settings, FolderOpen, Music, Image, Sparkles, List, Play, Clock, CheckCircle, XCircle } from 'lucide-react'
import { Home, Video, Settings, FolderOpen, Music, Image, Sparkles, List, Play, Clock, CheckCircle, XCircle, Layout } from 'lucide-react'
import { clsx } from 'clsx'
import { useAIVideoJobs } from '../stores/useAIVideoStore'
@@ -13,6 +13,7 @@ const Sidebar: React.FC = () => {
{ path: '/', icon: Home, label: '首页' },
{ path: '/editor', icon: Video, label: '编辑器' },
{ path: '/ai-video', icon: Sparkles, label: 'AI 视频' },
{ path: '/templates', icon: Layout, label: '模板管理' },
{ path: '/projects', icon: FolderOpen, label: '项目' },
{ path: '/media', icon: Image, label: '媒体库' },
{ path: '/audio', icon: Music, label: '音频' },

View File

@@ -27,7 +27,6 @@ const VideoSettings: React.FC<VideoSettingsProps> = ({
onChange={(e) => onDurationChange(e.target.value)}
className="input w-full"
>
<option value="3">3</option>
<option value="5">5</option>
<option value="10">10</option>
</select>
@@ -45,7 +44,6 @@ const VideoSettings: React.FC<VideoSettingsProps> = ({
className="input w-full"
>
<option value="lite">Lite ()</option>
<option value="standard">Standard ()</option>
<option value="pro">Pro ()</option>
</select>
</div>

View File

@@ -0,0 +1,433 @@
import React, { useState, useEffect } from 'react'
import { Upload, FolderOpen, Trash2, Eye, Download, Search, Filter, Grid, List } from 'lucide-react'
import { TemplateService } from '../services/tauri'
interface TemplateInfo {
id: string
name: string
description: string
thumbnail_path: string
draft_content_path: string
resources_path: string
created_at: string
updated_at: string
canvas_config: any
duration: number
material_count: number
track_count: number
tags: string[]
}
interface ImportResult {
status: boolean
msg: string
imported_count: number
failed_count: number
imported_templates: TemplateInfo[]
failed_templates: Array<{
name: string
error: string
}>
}
const TemplateManagePage: React.FC = () => {
const [templates, setTemplates] = useState<TemplateInfo[]>([])
const [loading, setLoading] = useState(false)
const [importing, setImporting] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
const [selectedTemplate, setSelectedTemplate] = useState<TemplateInfo | null>(null)
const [importResult, setImportResult] = useState<ImportResult | null>(null)
// Load templates on component mount
useEffect(() => {
loadTemplates()
}, [])
const loadTemplates = async () => {
try {
setLoading(true)
const result = await TemplateService.getTemplates()
if (result.status) {
setTemplates(result.templates || [])
} else {
console.error('Failed to load templates:', result.msg)
}
} catch (error) {
console.error('Error loading templates:', error)
} finally {
setLoading(false)
}
}
const handleBatchImport = async () => {
try {
// Select folder using Tauri dialog
const folderResult = await window.__TAURI_INVOKE__('select_folder')
if (!folderResult) return
setImporting(true)
setImportResult(null)
const result = await TemplateService.batchImportTemplates(folderResult)
setImportResult(result)
if (result.status && result.imported_count > 0) {
// Reload templates
await loadTemplates()
}
} catch (error) {
console.error('Import failed:', error)
setImportResult({
status: false,
msg: error instanceof Error ? error.message : 'Unknown error',
imported_count: 0,
failed_count: 0,
imported_templates: [],
failed_templates: []
})
} finally {
setImporting(false)
}
}
const handleDeleteTemplate = async (templateId: string) => {
if (!confirm('确定要删除这个模板吗?此操作不可撤销。')) return
try {
const result = await TemplateService.deleteTemplate(templateId)
if (result.status) {
setTemplates(templates.filter(t => t.id !== templateId))
if (selectedTemplate?.id === templateId) {
setSelectedTemplate(null)
}
} else {
alert('删除失败: ' + result.msg)
}
} catch (error) {
console.error('Delete failed:', error)
alert('删除失败: ' + (error instanceof Error ? error.message : 'Unknown error'))
}
}
const filteredTemplates = templates.filter(template =>
template.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
template.description.toLowerCase().includes(searchTerm.toLowerCase())
)
const formatDuration = (duration: number) => {
const seconds = Math.floor(duration / 1000000)
const minutes = Math.floor(seconds / 60)
const remainingSeconds = seconds % 60
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('zh-CN')
}
return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2"></h1>
<p className="text-gray-600"></p>
</div>
{/* Actions Bar */}
<div className="bg-white rounded-lg shadow-sm p-4 mb-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<button
onClick={handleBatchImport}
disabled={importing}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{importing ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
...
</>
) : (
<>
<Upload size={16} />
</>
)}
</button>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={16} />
<input
type="text"
placeholder="搜索模板..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => setViewMode('grid')}
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-blue-100 text-blue-600' : 'text-gray-400 hover:text-gray-600'}`}
>
<Grid size={16} />
</button>
<button
onClick={() => setViewMode('list')}
className={`p-2 rounded ${viewMode === 'list' ? 'bg-blue-100 text-blue-600' : 'text-gray-400 hover:text-gray-600'}`}
>
<List size={16} />
</button>
</div>
</div>
</div>
{/* Import Result */}
{importResult && (
<div className={`mb-6 p-4 rounded-lg ${importResult.status ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}>
<div className="flex items-center justify-between">
<div>
<h3 className={`font-medium ${importResult.status ? 'text-green-800' : 'text-red-800'}`}>
</h3>
<p className={`text-sm ${importResult.status ? 'text-green-600' : 'text-red-600'}`}>
{importResult.msg}
</p>
{importResult.imported_count > 0 && (
<p className="text-sm text-green-600 mt-1">
{importResult.imported_count}
</p>
)}
{importResult.failed_count > 0 && (
<details className="mt-2">
<summary className="text-sm text-red-600 cursor-pointer">
{importResult.failed_count} ()
</summary>
<div className="mt-2 space-y-1">
{importResult.failed_templates.map((failed, index) => (
<div key={index} className="text-xs text-red-500">
{failed.name}: {failed.error}
</div>
))}
</div>
</details>
)}
</div>
<button
onClick={() => setImportResult(null)}
className="text-gray-400 hover:text-gray-600"
>
×
</button>
</div>
</div>
)}
{/* Templates Grid/List */}
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-600 border-t-transparent"></div>
<span className="ml-2 text-gray-600">...</span>
</div>
) : filteredTemplates.length === 0 ? (
<div className="text-center py-12">
<FolderOpen className="mx-auto h-12 w-12 text-gray-400 mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2"></h3>
<p className="text-gray-600 mb-4">
{searchTerm ? '没有找到匹配的模板' : '点击上方按钮开始导入模板'}
</p>
{!searchTerm && (
<button
onClick={handleBatchImport}
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Upload size={16} />
</button>
)}
</div>
) : (
<div className={viewMode === 'grid' ? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6' : 'space-y-4'}>
{filteredTemplates.map((template) => (
<div
key={template.id}
className={`bg-white rounded-lg shadow-sm border border-gray-200 hover:shadow-md transition-shadow ${
viewMode === 'list' ? 'flex items-center p-4' : 'overflow-hidden'
}`}
>
{viewMode === 'grid' ? (
<>
{/* Thumbnail */}
<div className="h-32 bg-gray-100 flex items-center justify-center">
{template.thumbnail_path ? (
<img
src={template.thumbnail_path}
alt={template.name}
className="w-full h-full object-cover"
/>
) : (
<div className="text-gray-400">
<Grid size={32} />
</div>
)}
</div>
{/* Content */}
<div className="p-4">
<h3 className="font-medium text-gray-900 mb-1 truncate" title={template.name}>
{template.name}
</h3>
<p className="text-sm text-gray-600 mb-2 line-clamp-2">
{template.description}
</p>
<div className="text-xs text-gray-500 space-y-1">
<div>: {formatDuration(template.duration)}</div>
<div>: {template.material_count} </div>
<div>: {template.track_count} </div>
<div>: {formatDate(template.created_at)}</div>
</div>
{/* Actions */}
<div className="flex items-center justify-between mt-3 pt-3 border-t border-gray-100">
<button
onClick={() => setSelectedTemplate(template)}
className="flex items-center gap-1 text-blue-600 hover:text-blue-700 text-sm"
>
<Eye size={14} />
</button>
<button
onClick={() => handleDeleteTemplate(template.id)}
className="flex items-center gap-1 text-red-600 hover:text-red-700 text-sm"
>
<Trash2 size={14} />
</button>
</div>
</div>
</>
) : (
<>
{/* List View */}
<div className="flex-1">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium text-gray-900">{template.name}</h3>
<p className="text-sm text-gray-600">{template.description}</p>
</div>
<div className="flex items-center space-x-4 text-sm text-gray-500">
<span>{formatDuration(template.duration)}</span>
<span>{template.material_count} </span>
<span>{template.track_count} </span>
<span>{formatDate(template.created_at)}</span>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => setSelectedTemplate(template)}
className="p-2 text-blue-600 hover:text-blue-700"
>
<Eye size={16} />
</button>
<button
onClick={() => handleDeleteTemplate(template.id)}
className="p-2 text-red-600 hover:text-red-700"
>
<Trash2 size={16} />
</button>
</div>
</div>
</div>
</>
)}
</div>
))}
</div>
)}
{/* Template Detail Modal */}
{selectedTemplate && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[80vh] overflow-y-auto">
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-gray-900"></h2>
<button
onClick={() => setSelectedTemplate(null)}
className="text-gray-400 hover:text-gray-600"
>
×
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1"></label>
<p className="text-gray-900">{selectedTemplate.name}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1"></label>
<p className="text-gray-900">{selectedTemplate.description}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1"></label>
<p className="text-gray-900">{formatDuration(selectedTemplate.duration)}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1"></label>
<p className="text-gray-900">{selectedTemplate.material_count}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1"></label>
<p className="text-gray-900">{selectedTemplate.track_count}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1"></label>
<p className="text-gray-900">
{selectedTemplate.canvas_config?.width || 0} × {selectedTemplate.canvas_config?.height || 0}
</p>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1"></label>
<p className="text-gray-900">{new Date(selectedTemplate.created_at).toLocaleString('zh-CN')}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1"></label>
<p className="text-gray-900">{new Date(selectedTemplate.updated_at).toLocaleString('zh-CN')}</p>
</div>
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200">
<button
onClick={() => setSelectedTemplate(null)}
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
>
</button>
<button
onClick={() => handleDeleteTemplate(selectedTemplate.id)}
className="px-4 py-2 text-white bg-red-600 rounded-lg hover:bg-red-700 transition-colors"
>
</button>
</div>
</div>
</div>
</div>
</div>
)}
</div>
</div>
)
}
export default TemplateManagePage

View File

@@ -466,3 +466,59 @@ export class AIVideoService {
}
}
}
// Template management operations
export class TemplateService {
/**
* Batch import templates from a folder
*/
static async batchImportTemplates(sourceFolder: string): Promise<any> {
try {
const request = { source_folder: sourceFolder }
const result = await invoke('batch_import_templates', { request })
return JSON.parse(result as string)
} catch (error) {
console.error('Failed to batch import templates:', error)
throw error
}
}
/**
* Get all templates
*/
static async getTemplates(): Promise<any> {
try {
const result = await invoke('get_templates')
return JSON.parse(result as string)
} catch (error) {
console.error('Failed to get templates:', error)
throw error
}
}
/**
* Get a specific template
*/
static async getTemplate(templateId: string): Promise<any> {
try {
const result = await invoke('get_template', { templateId })
return JSON.parse(result as string)
} catch (error) {
console.error('Failed to get template:', error)
throw error
}
}
/**
* Delete a template
*/
static async deleteTemplate(templateId: string): Promise<any> {
try {
const result = await invoke('delete_template', { templateId })
return JSON.parse(result as string)
} catch (error) {
console.error('Failed to delete template:', error)
throw error
}
}
}

View File

@@ -0,0 +1,277 @@
import { invoke } from '@tauri-apps/api/tauri'
export interface TemplateInfo {
id: string
name: string
description: string
thumbnail_path: string
draft_content_path: string
resources_path: string
created_at: string
updated_at: string
canvas_config: any
duration: number
material_count: number
track_count: number
tags: string[]
}
export interface BatchImportRequest {
source_folder: string
}
export interface BatchImportResult {
status: boolean
msg: string
imported_count: number
failed_count: number
imported_templates: TemplateInfo[]
failed_templates: Array<{
name: string
error: string
}>
}
export interface TemplateResult {
status: boolean
msg?: string
template?: TemplateInfo
templates?: TemplateInfo[]
}
export class TemplateService {
/**
* Batch import templates from a folder
*/
static async batchImportTemplates(sourceFolder: string): Promise<BatchImportResult> {
try {
const request: BatchImportRequest = {
source_folder: sourceFolder
}
const response = await invoke<string>('batch_import_templates', { request })
const result = JSON.parse(response)
if (!result.status) {
throw new Error(result.error || result.msg || 'Import failed')
}
return result
} catch (error) {
console.error('Batch import templates failed:', error)
throw error
}
}
/**
* Get all templates
*/
static async getTemplates(): Promise<TemplateResult> {
try {
const response = await invoke<string>('get_templates')
const result = JSON.parse(response)
if (!result.status) {
throw new Error(result.error || result.msg || 'Failed to get templates')
}
return result
} catch (error) {
console.error('Get templates failed:', error)
throw error
}
}
/**
* Get a specific template
*/
static async getTemplate(templateId: string): Promise<TemplateResult> {
try {
const response = await invoke<string>('get_template', { templateId })
const result = JSON.parse(response)
if (!result.status) {
throw new Error(result.error || result.msg || 'Failed to get template')
}
return result
} catch (error) {
console.error('Get template failed:', error)
throw error
}
}
/**
* Delete a template
*/
static async deleteTemplate(templateId: string): Promise<TemplateResult> {
try {
const response = await invoke<string>('delete_template', { templateId })
const result = JSON.parse(response)
if (!result.status) {
throw new Error(result.error || result.msg || 'Failed to delete template')
}
return result
} catch (error) {
console.error('Delete template failed:', error)
throw error
}
}
/**
* Validate template folder structure
*/
static validateTemplateFolder(folderPath: string): boolean {
// This would be implemented to check if the folder contains valid template structure
// For now, we'll rely on the backend validation
return true
}
/**
* Generate template thumbnail
*/
static async generateThumbnail(templateId: string): Promise<string> {
// This would be implemented to generate a thumbnail from the template
// For now, return empty string
return ''
}
/**
* Export template
*/
static async exportTemplate(templateId: string, targetPath: string): Promise<boolean> {
try {
// This would be implemented to export a template to a specific location
// For now, just return true
return true
} catch (error) {
console.error('Export template failed:', error)
return false
}
}
/**
* Duplicate template
*/
static async duplicateTemplate(templateId: string, newName: string): Promise<TemplateResult> {
try {
// This would be implemented to create a copy of an existing template
// For now, just return a placeholder result
return {
status: false,
msg: 'Duplicate functionality not implemented yet'
}
} catch (error) {
console.error('Duplicate template failed:', error)
throw error
}
}
/**
* Update template metadata
*/
static async updateTemplate(templateId: string, updates: Partial<TemplateInfo>): Promise<TemplateResult> {
try {
// This would be implemented to update template metadata
// For now, just return a placeholder result
return {
status: false,
msg: 'Update functionality not implemented yet'
}
} catch (error) {
console.error('Update template failed:', error)
throw error
}
}
/**
* Search templates
*/
static async searchTemplates(query: string, filters?: {
tags?: string[]
duration_min?: number
duration_max?: number
material_count_min?: number
material_count_max?: number
}): Promise<TemplateResult> {
try {
// This would be implemented to search templates with filters
// For now, just get all templates and filter on frontend
const result = await this.getTemplates()
if (result.status && result.templates) {
const filteredTemplates = result.templates.filter(template =>
template.name.toLowerCase().includes(query.toLowerCase()) ||
template.description.toLowerCase().includes(query.toLowerCase())
)
return {
status: true,
templates: filteredTemplates
}
}
return result
} catch (error) {
console.error('Search templates failed:', error)
throw error
}
}
/**
* Get template statistics
*/
static async getTemplateStats(): Promise<{
total_templates: number
total_materials: number
total_size: number
most_used_tags: string[]
}> {
try {
const result = await this.getTemplates()
if (result.status && result.templates) {
const templates = result.templates
const totalTemplates = templates.length
const totalMaterials = templates.reduce((sum, t) => sum + t.material_count, 0)
// Extract all tags and count frequency
const tagCounts: { [key: string]: number } = {}
templates.forEach(template => {
template.tags.forEach(tag => {
tagCounts[tag] = (tagCounts[tag] || 0) + 1
})
})
const mostUsedTags = Object.entries(tagCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([tag]) => tag)
return {
total_templates: totalTemplates,
total_materials: totalMaterials,
total_size: 0, // Would need to calculate from file sizes
most_used_tags: mostUsedTags
}
}
return {
total_templates: 0,
total_materials: 0,
total_size: 0,
most_used_tags: []
}
} catch (error) {
console.error('Get template stats failed:', error)
return {
total_templates: 0,
total_materials: 0,
total_size: 0,
most_used_tags: []
}
}
}
}

7
template.md Normal file
View File

@@ -0,0 +1,7 @@
# 开发一个模板管理页面 需要有批量导入功能
1. 选择文件夹 批量上传模板
2. 解析文件夹下的每个子文件夹 每个子文件夹都是一个模板
3. 判断文件夹内是否有 draft_content.json 文件
4. 解析 draft_content.json 内容 分析轨道素材信息
5. 每个轨道有多个片段 每个片段 {id,material_id,source_timerange,target_timerange}
6. 根据material_id找到素材位置 然后 将素材单独存放在 软件 attachments/templates/模板名/resources目录下 并将对应素材的path替换成相对路径

243
test_template_manager.py Normal file
View File

@@ -0,0 +1,243 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Test script for template manager functionality
"""
import os
import json
import shutil
import tempfile
from pathlib import Path
def create_test_template(template_dir: Path, template_name: str):
"""Create a test template with draft_content.json"""
# Create template directory
template_path = template_dir / template_name
template_path.mkdir(parents=True, exist_ok=True)
# Create sample draft_content.json
draft_content = {
"version": "1.0",
"canvas_config": {
"width": 1920,
"height": 1080,
"fps": 30
},
"duration": 5000000, # 5 seconds in microseconds
"tracks": [
{
"id": "track_1",
"type": "video",
"segments": [
{
"id": "segment_1",
"material_id": "video_1",
"source_timerange": {"start": 0, "end": 3000000},
"target_timerange": {"start": 0, "end": 3000000}
}
]
},
{
"id": "track_2",
"type": "audio",
"segments": [
{
"id": "segment_2",
"material_id": "audio_1",
"source_timerange": {"start": 0, "end": 5000000},
"target_timerange": {"start": 0, "end": 5000000}
}
]
}
],
"materials": {
"videos": [
{
"id": "video_1",
"name": "sample_video.mp4",
"path": str(template_path / "sample_video.mp4"),
"duration": 3000000,
"width": 1920,
"height": 1080
}
],
"audios": [
{
"id": "audio_1",
"name": "sample_audio.mp3",
"path": str(template_path / "sample_audio.mp3"),
"duration": 5000000
}
],
"images": [
{
"id": "image_1",
"name": "sample_image.jpg",
"path": str(template_path / "sample_image.jpg"),
"width": 1920,
"height": 1080
}
]
}
}
# Save draft_content.json
with open(template_path / "draft_content.json", 'w', encoding='utf-8') as f:
json.dump(draft_content, f, ensure_ascii=False, indent=2)
# Create dummy media files
(template_path / "sample_video.mp4").touch()
(template_path / "sample_audio.mp3").touch()
(template_path / "sample_image.jpg").touch()
print(f"Created test template: {template_name}")
return template_path
def test_template_manager():
"""Test the template manager functionality"""
# Create temporary directory for test templates
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
print(f"Creating test templates in: {temp_path}")
# Create multiple test templates
templates = [
"Wedding_Template",
"Birthday_Template",
"Corporate_Template",
"Travel_Template"
]
for template_name in templates:
create_test_template(temp_path, template_name)
# Test the template manager
print("\nTesting template manager...")
try:
from python_core.services.template_manager import TemplateManager
manager = TemplateManager()
# Test batch import
print("Testing batch import...")
result = manager.batch_import_templates(str(temp_path))
print(f"Import result: {result}")
if result['status']:
print(f"Successfully imported {result['imported_count']} templates")
# Test get templates
print("\nTesting get templates...")
templates = manager.get_templates()
print(f"Found {len(templates)} templates")
for template in templates:
print(f" - {template.name} (ID: {template.id})")
print(f" Duration: {template.duration/1000000:.1f}s")
print(f" Materials: {template.material_count}")
print(f" Tracks: {template.track_count}")
# Test get specific template
if templates:
template_id = templates[0].id
print(f"\nTesting get specific template: {template_id}")
template = manager.get_template(template_id)
if template:
print(f"Retrieved template: {template.name}")
else:
print("Failed to retrieve template")
# Test delete template
if templates:
template_id = templates[0].id
print(f"\nTesting delete template: {template_id}")
success = manager.delete_template(template_id)
if success:
print("Template deleted successfully")
# Verify deletion
remaining_templates = manager.get_templates()
print(f"Remaining templates: {len(remaining_templates)}")
else:
print("Failed to delete template")
else:
print(f"Import failed: {result['msg']}")
except Exception as e:
print(f"Error testing template manager: {e}")
import traceback
traceback.print_exc()
def test_command_line():
"""Test the command line interface"""
print("\nTesting command line interface...")
# Create temporary directory for test templates
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create a test template
create_test_template(temp_path, "CLI_Test_Template")
# Test CLI commands
import subprocess
import sys
try:
# Test batch import
print("Testing CLI batch import...")
result = subprocess.run([
sys.executable, "-m", "python_core.services.template_manager",
"--action", "batch_import",
"--source_folder", str(temp_path)
], capture_output=True, text=True, cwd=".")
print(f"CLI Exit code: {result.returncode}")
print(f"CLI Stdout: {result.stdout}")
if result.stderr:
print(f"CLI Stderr: {result.stderr}")
if result.returncode == 0:
# Parse result
import json
cli_result = json.loads(result.stdout)
print(f"CLI Import result: {cli_result}")
# Test get templates
print("\nTesting CLI get templates...")
result = subprocess.run([
sys.executable, "-m", "python_core.services.template_manager",
"--action", "get_templates"
], capture_output=True, text=True, cwd=".")
if result.returncode == 0:
templates_result = json.loads(result.stdout)
print(f"CLI Templates: {len(templates_result.get('templates', []))}")
else:
print(f"CLI get templates failed: {result.stderr}")
else:
print("CLI batch import failed")
except Exception as e:
print(f"Error testing CLI: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
print("Template Manager Test Script")
print("=" * 50)
# Test the template manager class
test_template_manager()
# Test the command line interface
test_command_line()
print("\nTest completed!")