diff --git a/Cargo.lock b/Cargo.lock index 46a0ef1..59cf9f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2825,6 +2825,7 @@ dependencies = [ "url", "urlencoding", "uuid", + "veo3-scene-writer", "winapi", "zip 0.6.6", ] diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index b452384..ec29eb4 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -61,6 +61,7 @@ zip = "0.6" sysinfo = "0.30" comfyui-sdk = { path = "../../../cargos/comfyui-sdk" } tvai = { path = "../../../cargos/tvai" } +veo3-scene-writer = { path = "../../../cargos/veo3-scene-writer" } ffmpeg-sidecar = "2.0" [target.'cfg(windows)'.dependencies] diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 7a36045..05c3129 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -40,6 +40,7 @@ pub fn run() { .manage(commands::markdown_commands::MarkdownParserState::new()) .manage(commands::image_editing_commands::ImageEditingState::new()) .manage(commands::tvai_commands::TvaiState::new()) + .manage(commands::veo3_actor_define_commands::Veo3ActorDefineSessionManager::new(std::collections::HashMap::new())) .invoke_handler(tauri::generate_handler![ commands::project_commands::create_project, commands::project_commands::get_all_projects, @@ -779,7 +780,16 @@ pub fn run() { commands::tvai_commands::get_topaz_templates, commands::tvai_commands::generate_topaz_command, commands::tvai_commands::execute_topaz_video_processing, - commands::tvai_commands::execute_topaz_video_processing_with_progress + commands::tvai_commands::execute_topaz_video_processing_with_progress, + + // VEO3 角色定义命令 + commands::veo3_actor_define_commands::veo3_actor_define_check_availability, + commands::veo3_actor_define_commands::veo3_actor_define_send_message, + commands::veo3_actor_define_commands::veo3_actor_define_send_message_with_attachments, + commands::veo3_actor_define_commands::veo3_actor_define_get_conversation_history, + commands::veo3_actor_define_commands::veo3_actor_define_clear_conversation, + commands::veo3_actor_define_commands::veo3_actor_define_remove_session, + commands::veo3_actor_define_commands::veo3_actor_define_get_active_sessions ]) .setup(|app| { // 初始化日志系统 diff --git a/apps/desktop/src-tauri/src/presentation/commands/mod.rs b/apps/desktop/src-tauri/src/presentation/commands/mod.rs index 363b499..0abd980 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/mod.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/mod.rs @@ -45,6 +45,7 @@ pub mod volcano_video_commands; pub mod bowong_text_video_agent_commands; pub mod hedra_lipsync_commands; pub mod tvai_commands; +pub mod veo3_actor_define_commands; // 旧的 ComfyUI 命令(将被逐步替换) pub mod comfyui_commands; pub mod comfyui_sdk_commands; diff --git a/apps/desktop/src-tauri/src/presentation/commands/veo3_actor_define_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/veo3_actor_define_commands.rs new file mode 100644 index 0000000..a084072 --- /dev/null +++ b/apps/desktop/src-tauri/src/presentation/commands/veo3_actor_define_commands.rs @@ -0,0 +1,225 @@ +use std::collections::HashMap; +use tokio::sync::Mutex; +use tauri::State; +use serde::{Deserialize, Serialize}; +use veo3_scene_writer::Veo3ActorDefine; + +/// VEO3 角色定义响应结构 +#[derive(Debug, Serialize, Deserialize)] +pub struct Veo3ActorDefineResponse { + pub success: bool, + pub content: String, + pub error: Option, + pub conversation_history: Option>, +} + +/// VEO3 角色定义会话管理器 +pub type Veo3ActorDefineSessionManager = Mutex>; + +/// 检查 VEO3 角色定义服务可用性 +#[tauri::command] +pub async fn veo3_actor_define_check_availability() -> Result<(), String> { + // 尝试创建一个实例来检查服务是否可用 + match Veo3ActorDefine::new().await { + Ok(_) => Ok(()), + Err(e) => Err(format!("VEO3 角色定义服务不可用: {}", e)), + } +} + +/// 发送消息到 VEO3 角色定义工具 +#[tauri::command] +pub async fn veo3_actor_define_send_message( + session_id: String, + message: String, + session_manager: State<'_, Veo3ActorDefineSessionManager>, +) -> Result { + // 检查会话是否存在,如果不存在则创建 + let session_exists = { + let sessions = session_manager.lock().await; + sessions.contains_key(&session_id) + }; + + if !session_exists { + let new_actor_define = Veo3ActorDefine::new() + .await + .map_err(|e| format!("创建 VEO3 角色定义实例失败: {}", e))?; + + let mut sessions = session_manager.lock().await; + sessions.insert(session_id.clone(), new_actor_define); + } + + // 发送消息 + let (response_result, conversation_history) = { + let mut sessions = session_manager.lock().await; + let actor_define = sessions.get_mut(&session_id).unwrap(); + + let response = actor_define.send_message(&message).await; + let history = actor_define.get_conversation_history().clone(); + (response, history) + }; + + match response_result { + Ok(response) => Ok(Veo3ActorDefineResponse { + success: true, + content: response, + error: None, + conversation_history: Some(conversation_history), + }), + Err(e) => Ok(Veo3ActorDefineResponse { + success: false, + content: String::new(), + error: Some(format!("发送消息失败: {}", e)), + conversation_history: None, + }), + } +} + +/// 发送带附件的消息到 VEO3 角色定义工具 +#[tauri::command] +pub async fn veo3_actor_define_send_message_with_attachments( + session_id: String, + message: String, + attachment_paths: Vec, + session_manager: State<'_, Veo3ActorDefineSessionManager>, +) -> Result { + // 检查会话是否存在,如果不存在则创建 + let session_exists = { + let sessions = session_manager.lock().await; + sessions.contains_key(&session_id) + }; + + if !session_exists { + let new_actor_define = Veo3ActorDefine::new() + .await + .map_err(|e| format!("创建 VEO3 角色定义实例失败: {}", e))?; + + let mut sessions = session_manager.lock().await; + sessions.insert(session_id.clone(), new_actor_define); + } + + // 转换路径为字符串引用 + let attachment_refs: Vec<&str> = attachment_paths.iter().map(|s| s.as_str()).collect(); + + // 发送带附件的消息 + let (response_result, conversation_history) = { + let mut sessions = session_manager.lock().await; + let actor_define = sessions.get_mut(&session_id).unwrap(); + + let response = actor_define.send_message_with_attachments(&message, attachment_refs).await; + let history = actor_define.get_conversation_history().clone(); + (response, history) + }; + + match response_result { + Ok(response) => Ok(Veo3ActorDefineResponse { + success: true, + content: response, + error: None, + conversation_history: Some(conversation_history), + }), + Err(e) => Ok(Veo3ActorDefineResponse { + success: false, + content: String::new(), + error: Some(format!("发送带附件消息失败: {}", e)), + conversation_history: None, + }), + } +} + +/// 获取 VEO3 角色定义会话历史记录 +#[tauri::command] +pub async fn veo3_actor_define_get_conversation_history( + session_id: String, + session_manager: State<'_, Veo3ActorDefineSessionManager>, +) -> Result, String> { + let sessions = session_manager.lock().await; + + if let Some(actor_define) = sessions.get(&session_id) { + Ok(actor_define.get_conversation_history().clone()) + } else { + Ok(Vec::new()) + } +} + +/// 清除 VEO3 角色定义会话历史记录 +#[tauri::command] +pub async fn veo3_actor_define_clear_conversation( + session_id: String, + session_manager: State<'_, Veo3ActorDefineSessionManager>, +) -> Result<(), String> { + let mut sessions = session_manager.lock().await; + + if let Some(actor_define) = sessions.get_mut(&session_id) { + actor_define.clear_conversation(); + } + + Ok(()) +} + +/// 删除 VEO3 角色定义会话 +#[tauri::command] +pub async fn veo3_actor_define_remove_session( + session_id: String, + session_manager: State<'_, Veo3ActorDefineSessionManager>, +) -> Result<(), String> { + let mut sessions = session_manager.lock().await; + sessions.remove(&session_id); + Ok(()) +} + +/// 获取所有活跃的 VEO3 角色定义会话ID +#[tauri::command] +pub async fn veo3_actor_define_get_active_sessions( + session_manager: State<'_, Veo3ActorDefineSessionManager>, +) -> Result, String> { + let sessions = session_manager.lock().await; + Ok(sessions.keys().cloned().collect()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + + #[tokio::test] + async fn test_veo3_actor_define_check_availability() { + // 注意:这个测试需要有效的 API 配置才能通过 + if std::env::var("GEMINI_BEARER_TOKEN").is_ok() { + let result = veo3_actor_define_check_availability().await; + assert!(result.is_ok()); + } + } + + #[tokio::test] + async fn test_session_management() { + let session_manager = Arc::new(Veo3ActorDefineSessionManager::new(HashMap::new())); + let session_id = "test-session".to_string(); + + // 测试获取活跃会话(应该为空) + let active_sessions = veo3_actor_define_get_active_sessions( + tauri::State::from(session_manager.clone()) + ).await.unwrap(); + assert!(active_sessions.is_empty()); + + // 测试获取不存在会话的历史记录 + let history = veo3_actor_define_get_conversation_history( + session_id.clone(), + tauri::State::from(session_manager.clone()) + ).await.unwrap(); + assert!(history.is_empty()); + + // 测试清除不存在会话的历史记录 + let result = veo3_actor_define_clear_conversation( + session_id.clone(), + tauri::State::from(session_manager.clone()) + ).await; + assert!(result.is_ok()); + + // 测试删除不存在的会话 + let result = veo3_actor_define_remove_session( + session_id, + tauri::State::from(session_manager) + ).await; + assert!(result.is_ok()); + } +} diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 08f189a..624e8bc 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -36,6 +36,7 @@ import OmniHumanDetectionTool from './pages/tools/OmniHumanDetectionTool'; import { EnrichedAnalysisDemo } from './pages/tools/EnrichedAnalysisDemo'; import AiModelFaceHairFixTool from './pages/tools/AiModelFaceHairFixTool'; import TopazVideoAITool from './pages/tools/TopazVideoAITool'; +import Veo3ActorDefineTool from './pages/tools/Veo3ActorDefineTool'; import CommandGenerator from './components/CommandGenerator'; import MaterialCenter from './pages/MaterialCenter'; import VideoGeneration from './pages/VideoGeneration'; @@ -183,6 +184,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/apps/desktop/src/data/tools.ts b/apps/desktop/src/data/tools.ts index e675c34..5cc0382 100644 --- a/apps/desktop/src/data/tools.ts +++ b/apps/desktop/src/data/tools.ts @@ -11,6 +11,7 @@ import { ImagePlus, Frame, Sparkles, + User, } from 'lucide-react'; import { Tool, ToolCategory, ToolStatus } from '../types/tool'; @@ -19,6 +20,21 @@ import { Tool, ToolCategory, ToolStatus } from '../types/tool'; * 定义所有可用的工具及其属性 */ export const TOOLS_DATA: Tool[] = [ + { + id: 'veo3-actor-define', + name: 'VEO3 角色定义工具', + description: '专业的影视角色档案创建助手,支持图片上传和智能对话,生成详细的角色描述和声音特征', + longDescription: '基于 VEO3 场景写作系统的专业角色定义工具,集成 Gemini AI 提供智能对话功能。支持上传参考图片进行角色分析,或通过对话生成全新角色。能够创建包含视觉特征、声音特征、性格特点等完整角色档案,输出 JSON 格式的结构化数据。适用于影视制作、游戏开发、创意写作等需要角色设定的场景。', + icon: User, + route: '/tools/veo3-actor-define', + category: ToolCategory.AI_TOOLS, + status: ToolStatus.STABLE, + tags: ['角色定义', 'VEO3', '影视制作', 'AI对话', '图片分析', '角色档案'], + isNew: true, + isPopular: true, + version: '1.0.0', + lastUpdated: '2025-08-15' + }, { id: 'topaz-video-ai', name: 'Topaz Video AI 参数配置器', diff --git a/apps/desktop/src/pages/tools/Veo3ActorDefineTool.tsx b/apps/desktop/src/pages/tools/Veo3ActorDefineTool.tsx new file mode 100644 index 0000000..c71e0ac --- /dev/null +++ b/apps/desktop/src/pages/tools/Veo3ActorDefineTool.tsx @@ -0,0 +1,476 @@ +import React, { useState, useCallback, useRef, useEffect } from 'react'; +import { + Send, + AlertCircle, + Sparkles, + Upload, + FileImage, + X, + User, + MessageSquare, + RefreshCw, + Download, + Copy, + CheckCircle +} from 'lucide-react'; +import { open } from '@tauri-apps/plugin-dialog'; +import { + veo3ActorDefineService, + Veo3ActorDefineResponse +} from '../../services/veo3ActorDefineService'; + +/** + * VEO3 角色定义消息接口 + */ +interface ActorDefineMessage { + id: string; + type: 'user' | 'assistant'; + content: string; + timestamp: Date; + status?: 'sending' | 'sent' | 'error'; + attachments?: string[]; +} + +/** + * VEO3 角色定义工具页面 + * 集成 ag-ui 实现对话和本地附件选择功能 + */ +export const Veo3ActorDefineTool: React.FC = () => { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [selectedFiles, setSelectedFiles] = useState([]); + const [serviceAvailable, setServiceAvailable] = useState(null); + const [copiedMessageId, setCopiedMessageId] = useState(null); + + const inputRef = useRef(null); + const messagesEndRef = useRef(null); + + // 检查服务可用性 + useEffect(() => { + const checkService = async () => { + const available = await veo3ActorDefineService.checkServiceAvailability(); + setServiceAvailable(available); + }; + checkService(); + }, []); + + // 自动滚动到底部 + const scrollToBottom = useCallback(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, []); + + useEffect(() => { + scrollToBottom(); + }, [messages, scrollToBottom]); + + // 生成消息ID + const generateMessageId = useCallback(() => { + return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; + }, []); + + // 选择文件 + const handleSelectFiles = useCallback(async () => { + try { + const selected = await open({ + multiple: true, + filters: [ + { + name: 'Images', + extensions: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'] + }, + { + name: 'All Files', + extensions: ['*'] + } + ] + }); + + if (selected && Array.isArray(selected)) { + setSelectedFiles(prev => [...prev, ...selected]); + } else if (selected && typeof selected === 'string') { + setSelectedFiles(prev => [...prev, selected]); + } + } catch (error) { + console.error('文件选择失败:', error); + setError('文件选择失败'); + } + }, []); + + // 移除选中的文件 + const removeSelectedFile = useCallback((index: number) => { + setSelectedFiles(prev => prev.filter((_, i) => i !== index)); + }, []); + + // 清空选中的文件 + const clearSelectedFiles = useCallback(() => { + setSelectedFiles([]); + }, []); + + // 发送消息 + const handleSendMessage = useCallback(async () => { + if ((!input.trim() && selectedFiles.length === 0) || isLoading) return; + + const userMessage: ActorDefineMessage = { + id: generateMessageId(), + type: 'user', + content: input.trim() || '(附件)', + timestamp: new Date(), + status: 'sent', + attachments: selectedFiles.length > 0 ? [...selectedFiles] : undefined + }; + + const assistantMessage: ActorDefineMessage = { + id: generateMessageId(), + type: 'assistant', + content: '', + timestamp: new Date(), + status: 'sending' + }; + + setMessages(prev => [...prev, userMessage, assistantMessage]); + setInput(''); + setSelectedFiles([]); + setIsLoading(true); + setError(null); + + try { + let response: Veo3ActorDefineResponse; + + if (selectedFiles.length > 0) { + response = await veo3ActorDefineService.sendMessageWithAttachments( + userMessage.content, + selectedFiles + ); + } else { + response = await veo3ActorDefineService.sendMessage(userMessage.content); + } + + if (response.success) { + setMessages(prev => prev.map(msg => + msg.id === assistantMessage.id + ? { + ...msg, + content: response.content, + status: 'sent' + } + : msg + )); + } else { + throw new Error(response.error || '发送失败'); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : '未知错误'; + setError(errorMessage); + + setMessages(prev => prev.map(msg => + msg.id === assistantMessage.id + ? { + ...msg, + content: `抱歉,处理时出现错误:${errorMessage}`, + status: 'error' + } + : msg + )); + } finally { + setIsLoading(false); + } + }, [input, selectedFiles, isLoading, generateMessageId]); + + // 处理键盘事件 + const handleKeyPress = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }, [handleSendMessage]); + + // 清除会话 + const handleClearConversation = useCallback(async () => { + const success = await veo3ActorDefineService.clearConversation(); + if (success) { + setMessages([]); + setError(null); + } + }, []); + + // 重置会话 + const handleResetSession = useCallback(() => { + veo3ActorDefineService.resetSession(); + setMessages([]); + setError(null); + }, []); + + // 复制消息内容 + const handleCopyMessage = useCallback(async (content: string, messageId: string) => { + try { + await navigator.clipboard.writeText(content); + setCopiedMessageId(messageId); + setTimeout(() => setCopiedMessageId(null), 2000); + } catch (error) { + console.error('复制失败:', error); + } + }, []); + + // 如果服务不可用,显示错误状态 + if (serviceAvailable === false) { + return ( +
+ +

VEO3 角色定义服务不可用

+

+ 请确保后端服务正在运行,并且已正确配置 Gemini API。 +

+ +
+ ); + } + + return ( +
+ {/* 头部 */} +
+
+
+
+ +
+
+

VEO3 角色定义工具

+

专业的影视角色档案创建助手

+
+
+ +
+ + +
+
+
+ + {/* 聊天消息区域 */} +
+ {messages.length === 0 ? ( +
+
+ +
+

+ 我是 VEO3 角色定义专家,帮助您创建详细的影视角色档案。您可以上传参考图片或描述角色特征。 +

+
+ {[ + '我想创建一个角色档案', + '上传角色参考图片', + '生成一个新角色', + '定义角色的声音特征' + ].map((suggestion, index) => ( + + ))} +
+
+ ) : ( + <> + {messages.map((message) => ( +
+
+ {/* 消息内容 */} +
{message.content}
+ + {/* 附件显示 */} + {message.attachments && message.attachments.length > 0 && ( +
+ {message.attachments.map((file, index) => ( +
+ + {file.split('/').pop() || file} +
+ ))} +
+ )} + + {/* 消息操作 */} + {message.type === 'assistant' && message.status === 'sent' && ( +
+ +
+ )} + + {/* 状态指示 */} + {message.status === 'sending' && ( +
+
+ 正在处理... +
+ )} + + {message.status === 'error' && ( +
+ + 发送失败 +
+ )} +
+
+ ))} +
+ + )} +
+ + {/* 错误提示 */} + {error && ( +
+ + {error} + +
+ )} + + {/* 输入区域 */} +
+ {/* 选中的文件显示 */} + {selectedFiles.length > 0 && ( +
+
+ + 已选择 {selectedFiles.length} 个文件 + + +
+
+ {selectedFiles.map((file, index) => ( +
+ + {file.split('/').pop() || file} + +
+ ))} +
+
+ )} + + {/* 输入框 */} +
+
+ + +
+