feat: 添加 VEO3 角色定义工具

- 创建 VEO3ActorDefineTool 页面组件,集成聊天界面和文件选择功能
- 添加 veo3ActorDefineService 服务层,封装与 Rust 后端的通信逻辑
- 实现 Tauri 命令支持,调用 veo3-scene-writer crate
- 更新工具数据配置,添加 VEO3 角色生成工具
- 支持文本消息和图片附件上传
- 提供会话管理和历史记录功能
- 集成 ag-ui 设计标准,提供优秀的用户体验
This commit is contained in:
imeepos 2025-08-15 18:29:18 +08:00
parent 577b539ee2
commit c7268ba5b1
10 changed files with 932 additions and 61 deletions

1
Cargo.lock generated
View File

@ -2825,6 +2825,7 @@ dependencies = [
"url",
"urlencoding",
"uuid",
"veo3-scene-writer",
"winapi",
"zip 0.6.6",
]

View File

@ -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]

View File

@ -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| {
// 初始化日志系统

View File

@ -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;

View File

@ -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<String>,
pub conversation_history: Option<Vec<String>>,
}
/// VEO3 角色定义会话管理器
pub type Veo3ActorDefineSessionManager = Mutex<HashMap<String, Veo3ActorDefine>>;
/// 检查 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<Veo3ActorDefineResponse, String> {
// 检查会话是否存在,如果不存在则创建
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<String>,
session_manager: State<'_, Veo3ActorDefineSessionManager>,
) -> Result<Veo3ActorDefineResponse, String> {
// 检查会话是否存在,如果不存在则创建
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<Vec<String>, 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<Vec<String>, 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());
}
}

View File

@ -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() {
<Route path="/tools/enriched-analysis-demo" element={<EnrichedAnalysisDemo />} />
<Route path="/tools/ai-model-face-hair-fix" element={<AiModelFaceHairFixTool />} />
<Route path="/tools/topaz-video-ai" element={<TopazVideoAITool />} />
<Route path="/tools/veo3-actor-define" element={<Veo3ActorDefineTool />} />
<Route path="/tools/command-generator" element={<CommandGenerator />} />
</Routes>
</div>

View File

@ -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 参数配置器',

View File

@ -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<ActorDefineMessage[]>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
const [serviceAvailable, setServiceAvailable] = useState<boolean | null>(null);
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(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 (
<div className="flex flex-col items-center justify-center h-full p-8 text-center">
<AlertCircle className="w-16 h-16 text-red-500 mb-4" />
<h2 className="text-xl font-semibold text-gray-800 mb-2">VEO3 </h2>
<p className="text-gray-600 mb-4">
Gemini API
</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
</button>
</div>
);
}
return (
<div className="flex flex-col h-full bg-gradient-to-br from-purple-50 to-pink-50">
{/* 头部 */}
<div className="flex-shrink-0 bg-white border-b border-gray-200 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-pink-500 rounded-lg flex items-center justify-center">
<User className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-xl font-semibold text-gray-800">VEO3 </h1>
<p className="text-sm text-gray-600"></p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleClearConversation}
className="px-3 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors text-sm"
>
<MessageSquare className="w-4 h-4 mr-1 inline" />
</button>
<button
onClick={handleResetSession}
className="px-3 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors text-sm"
>
<RefreshCw className="w-4 h-4 mr-1 inline" />
</button>
</div>
</div>
</div>
{/* 聊天消息区域 */}
<div className="flex-1 p-4 space-y-6 overflow-y-auto">
{messages.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center">
<div className="w-20 h-20 bg-gradient-to-br from-purple-100 to-pink-100 rounded-full flex items-center justify-center mb-6">
<Sparkles className="w-10 h-10 text-purple-500" />
</div>
<p className="text-gray-600 max-w-lg mb-8 text-lg leading-relaxed">
VEO3
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-w-2xl">
{[
'我想创建一个角色档案',
'上传角色参考图片',
'生成一个新角色',
'定义角色的声音特征'
].map((suggestion, index) => (
<button
key={index}
onClick={() => setInput(suggestion)}
className="px-3 py-2 text-sm text-purple-600 bg-purple-50 hover:bg-purple-100 rounded-lg transition-all duration-200 hover:shadow-md hover:scale-105"
>
{suggestion}
</button>
))}
</div>
</div>
) : (
<>
{messages.map((message) => (
<div
key={message.id}
className={`flex ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[80%] rounded-lg p-4 ${
message.type === 'user'
? 'bg-purple-500 text-white'
: 'bg-white border border-gray-200 text-gray-800'
}`}
>
{/* 消息内容 */}
<div className="whitespace-pre-wrap">{message.content}</div>
{/* 附件显示 */}
{message.attachments && message.attachments.length > 0 && (
<div className="mt-2 space-y-1">
{message.attachments.map((file, index) => (
<div key={index} className="flex items-center gap-2 text-sm opacity-80">
<FileImage className="w-4 h-4" />
<span>{file.split('/').pop() || file}</span>
</div>
))}
</div>
)}
{/* 消息操作 */}
{message.type === 'assistant' && message.status === 'sent' && (
<div className="flex items-center gap-2 mt-2 pt-2 border-t border-gray-100">
<button
onClick={() => handleCopyMessage(message.content, message.id)}
className="flex items-center gap-1 text-xs text-gray-500 hover:text-gray-700 transition-colors"
>
{copiedMessageId === message.id ? (
<>
<CheckCircle className="w-3 h-3" />
</>
) : (
<>
<Copy className="w-3 h-3" />
</>
)}
</button>
</div>
)}
{/* 状态指示 */}
{message.status === 'sending' && (
<div className="flex items-center gap-2 mt-2 text-sm opacity-70">
<div className="animate-spin w-4 h-4 border-2 border-current border-t-transparent rounded-full"></div>
<span>...</span>
</div>
)}
{message.status === 'error' && (
<div className="flex items-center gap-2 mt-2 text-sm text-red-500">
<AlertCircle className="w-4 h-4" />
<span></span>
</div>
)}
</div>
</div>
))}
<div ref={messagesEndRef} />
</>
)}
</div>
{/* 错误提示 */}
{error && (
<div className="mx-4 mb-2 p-3 bg-red-50 border border-red-200 rounded-lg flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-red-500 flex-shrink-0" />
<span className="text-red-700 text-sm">{error}</span>
<button
onClick={() => setError(null)}
className="ml-auto text-red-600 hover:text-red-800 text-sm font-medium"
>
</button>
</div>
)}
{/* 输入区域 */}
<div className="flex-shrink-0 bg-white border-t border-gray-200">
{/* 选中的文件显示 */}
{selectedFiles.length > 0 && (
<div className="border-b border-gray-100 p-3">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700">
{selectedFiles.length}
</span>
<button
onClick={clearSelectedFiles}
className="text-xs text-gray-500 hover:text-gray-700"
>
</button>
</div>
<div className="flex flex-wrap gap-2">
{selectedFiles.map((file, index) => (
<div
key={index}
className="flex items-center gap-2 bg-gray-100 rounded-lg px-3 py-1 text-sm"
>
<FileImage className="w-4 h-4 text-gray-500" />
<span className="max-w-32 truncate">{file.split('/').pop() || file}</span>
<button
onClick={() => removeSelectedFile(index)}
className="text-gray-400 hover:text-gray-600"
>
<X className="w-3 h-3" />
</button>
</div>
))}
</div>
</div>
)}
{/* 输入框 */}
<div className="p-4">
<div className="flex gap-3 items-end">
<button
onClick={handleSelectFiles}
className="px-3 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 transition-colors flex items-center gap-2 flex-shrink-0"
>
<Upload className="w-4 h-4" />
</button>
<div className="flex-1 relative">
<textarea
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyPress}
placeholder="描述您想要创建的角色,或上传参考图片..."
disabled={isLoading}
className="w-full px-4 py-3 border border-gray-300 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed"
rows={1}
style={{
minHeight: '48px',
maxHeight: '120px',
height: 'auto'
}}
onInput={(e) => {
const target = e.target as HTMLTextAreaElement;
target.style.height = 'auto';
target.style.height = `${Math.min(target.scrollHeight, 120)}px`;
}}
/>
</div>
<button
onClick={handleSendMessage}
disabled={(!input.trim() && selectedFiles.length === 0) || isLoading}
className="px-4 bg-purple-500 text-white rounded-lg hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 disabled:bg-gray-300 disabled:cursor-not-allowed transition-all duration-200 flex items-center justify-center flex-shrink-0"
style={{ height: '48px', minHeight: '48px' }}
>
{isLoading ? (
<div className="animate-spin w-5 h-5 border-2 border-white border-t-transparent rounded-full"></div>
) : (
<Send className="w-5 h-5" />
)}
</button>
</div>
<div className="mt-2 text-center text-xs text-gray-500">
<span> Enter Shift + Enter </span>
</div>
</div>
</div>
</div>
);
};
export default Veo3ActorDefineTool;

View File

@ -0,0 +1,193 @@
import { invoke } from '@tauri-apps/api/core';
/**
* VEO3
*/
export interface Veo3ActorDefineMessage {
content: string;
attachments?: string[];
}
/**
* VEO3
*/
export interface Veo3ActorDefineResponse {
success: boolean;
content: string;
error?: string;
conversation_history?: string[];
}
/**
* VEO3
*/
export interface Veo3ActorDefineSession {
session_id: string;
conversation_history: string[];
created_at: string;
last_updated: string;
}
/**
* VEO3
* Rust veo3-scene-writer crate
*/
export class Veo3ActorDefineService {
private static instance: Veo3ActorDefineService;
private sessionId: string;
private constructor() {
this.sessionId = `veo3-actor-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
}
/**
*
*/
public static getInstance(): Veo3ActorDefineService {
if (!Veo3ActorDefineService.instance) {
Veo3ActorDefineService.instance = new Veo3ActorDefineService();
}
return Veo3ActorDefineService.instance;
}
/**
* VEO3
*/
public async sendMessage(message: string): Promise<Veo3ActorDefineResponse> {
try {
const response = await invoke<Veo3ActorDefineResponse>('veo3_actor_define_send_message', {
sessionId: this.sessionId,
message: message
});
return response;
} catch (error) {
console.error('VEO3 角色定义消息发送失败:', error);
return {
success: false,
content: '',
error: error instanceof Error ? error.message : '未知错误'
};
}
}
/**
* VEO3
*/
public async sendMessageWithAttachments(
message: string,
attachmentPaths: string[]
): Promise<Veo3ActorDefineResponse> {
try {
const response = await invoke<Veo3ActorDefineResponse>('veo3_actor_define_send_message_with_attachments', {
sessionId: this.sessionId,
message: message,
attachmentPaths: attachmentPaths
});
return response;
} catch (error) {
console.error('VEO3 角色定义带附件消息发送失败:', error);
return {
success: false,
content: '',
error: error instanceof Error ? error.message : '未知错误'
};
}
}
/**
*
*/
public async getConversationHistory(): Promise<string[]> {
try {
const history = await invoke<string[]>('veo3_actor_define_get_conversation_history', {
sessionId: this.sessionId
});
return history;
} catch (error) {
console.error('获取 VEO3 角色定义会话历史失败:', error);
return [];
}
}
/**
*
*/
public async clearConversation(): Promise<boolean> {
try {
await invoke('veo3_actor_define_clear_conversation', {
sessionId: this.sessionId
});
return true;
} catch (error) {
console.error('清除 VEO3 角色定义会话历史失败:', error);
return false;
}
}
/**
* ID
*/
public getSessionId(): string {
return this.sessionId;
}
/**
* ID
*/
public resetSession(): void {
this.sessionId = `veo3-actor-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
}
/**
*
*/
public async checkServiceAvailability(): Promise<boolean> {
try {
await invoke('veo3_actor_define_check_availability');
return true;
} catch (error) {
console.error('VEO3 角色定义服务不可用:', error);
return false;
}
}
}
/**
*
*/
export const veo3ActorDefineService = Veo3ActorDefineService.getInstance();
/**
* 便
*/
export const sendVeo3ActorDefineMessage = (message: string): Promise<Veo3ActorDefineResponse> => {
return veo3ActorDefineService.sendMessage(message);
};
/**
* 便
*/
export const sendVeo3ActorDefineMessageWithAttachments = (
message: string,
attachmentPaths: string[]
): Promise<Veo3ActorDefineResponse> => {
return veo3ActorDefineService.sendMessageWithAttachments(message, attachmentPaths);
};
/**
* 便
*/
export const getVeo3ActorDefineConversationHistory = (): Promise<string[]> => {
return veo3ActorDefineService.getConversationHistory();
};
/**
* 便
*/
export const clearVeo3ActorDefineConversation = (): Promise<boolean> => {
return veo3ActorDefineService.clearConversation();
};

View File

@ -1,60 +1,6 @@
Your Role: You are an expert cinematic video prompt writer, specializing in creating compelling and VEO3-consistent characters and dynamic scenes. Your primary goal is to guide me through a precise process to define a character with exact replication of visual details, and then generate single-paragraph, actionable prompts for short, cinematic video clips. The user prefer Chinese instruction, but all data outcome and prompt generated for VEO3 must be in english.
Phase 1: Character profile(s) import
To begin, how do you want to define your consistent character(s)? Import existing character profile(s) contain both visual feature and voice feature. If so continue to Phase 2 using the imported character profile(s).
Phase 2: Cinematic Scene Generation
Once your consistent character are imported, let me know how would you like to generate a cinematic scene with them.
Option A: Upload an example video clip so I can analyze and copy from example video clip
Option B: Please provide the following details for your 8-second video clip
Both approach should consider the following factor:
Character(s): How many character(s) in the scene? What's their relationship?
Scene Description: What's happening in the scene? Include the character's precise actions, the specific setting details, and the overall mood.
Dialogue (Optional): What does character(s) say? Keep it concise, about 15-20 words, for an 8-second clip.
Camera Work: What exact camera angle and movement do you envision? (e.g., "tight close-up on character's eyes, slowly pushing in," "wide shot from behind character, tracking as they walk," "low angle shot, handheld, with a subtle tilt.")
Camera movement: 
Eye Level: Subject's eye height perspective.
High Angle: Camera looking down.
Worm's Eye: Camera looking up from a low point.
Dolly Shot: Camera moves on a track, maintaining distance.
Zoom Shot: Lens changes focal length (in or out).
Pan Shot: Camera rotates horizontally from fixed position.
Tracking Shot: Camera follows a subject.
Lighting: Describe the precise lighting. (e.g., "soft, dappled sunlight filtering through leaves," "harsh, overhead fluorescent lighting creating strong shadows," "warm, golden hour glow from the west.")
I'll ask clarifying questions to ensure I capture your vision perfectly.
Phase 3: Generate Cinematic Prompt
Based on all the information, I will generate a single, VEO3-optimized paragraph prompt ready for you to copy and paste. This prompt will include:
Your imported detailed consistent character's visual description.
Their defined voice profile.
The complete cinematic scene setup, including precise actions, dialogue, camera work, lighting, mood, and setting.
Then I should confirm which format user want as output
Option A: Raw text
Option B: JSON format, which layout each key factor into a key value JSON object data
Option C: YAML format, which layout each key factor into a YAML object data
Continue Generating Scenes
After a scene is generated, I will ask if you'd like to process with the following options.
To create another cinematic scene with the same character, If so, we'll go directly to Phase 2 (Cinematic Scene Generation) to define the next clip. For multiple scenes you must always describe the person details in full for each prompt. Each prompt is separate from the other so full description is the only way it knows what the character looks like
### Veo3ActorDefine 角色生成
> 开发一个 便捷工具 放到 apps\desktop
- 集成 ag-ui 实现对话/本地附件选择
### Veo3SceneWriter 场景提示词生成
> 开发一个 便捷工具 放到 apps\desktop
- 集成 ag-ui 实现对话/本地附件选择