feat: 添加 VEO3 场景写作和角色定义 SDK
- 新增 gemini-sdk: 基于 Gemini AI 的通用 SDK - 新增 veo3-scene-writer: VEO3 场景写作专用工具 - 支持图片/视频附件分析 - 多轮会话功能 - VEO3 专业提示词集成 - 角色一致性管理 - 新增 Veo3ActorDefine: 角色定义专用工具 - 添加完整的示例和文档 - 支持多种输出格式 (文本/JSON/YAML)
This commit is contained in:
22
Cargo.lock
generated
22
Cargo.lock
generated
@@ -1656,6 +1656,20 @@ dependencies = [
|
||||
"x11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gemini-sdk"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"reqwest 0.11.27",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -5885,6 +5899,14 @@ version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "veo3-scene-writer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gemini-sdk",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
|
||||
@@ -3,6 +3,8 @@ resolver = "2"
|
||||
members = [
|
||||
"apps/desktop/src-tauri",
|
||||
"cargos/tvai",
|
||||
"cargos/gemini-sdk",
|
||||
"cargos/veo3-scene-writer",
|
||||
# "packages/services/rust-service", # 可选的 Rust 微服务 (暂时注释掉)
|
||||
]
|
||||
|
||||
|
||||
1652
cargos/gemini-sdk/Cargo.lock
generated
Normal file
1652
cargos/gemini-sdk/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
cargos/gemini-sdk/Cargo.toml
Normal file
26
cargos/gemini-sdk/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "gemini-sdk"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Augment Code <dev@augmentcode.com>"]
|
||||
description = "A simple and powerful Rust SDK for Google Gemini AI services"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/your-org/mixvideo"
|
||||
keywords = ["gemini", "ai", "sdk", "google", "llm"]
|
||||
categories = ["api-bindings", "web-programming"]
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
reqwest = { version = "0.11", features = ["json", "multipart"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
anyhow = "1.0"
|
||||
base64 = "0.22"
|
||||
thiserror = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
|
||||
[[example]]
|
||||
name = "basic_usage"
|
||||
path = "examples/basic_usage.rs"
|
||||
189
cargos/gemini-sdk/README.md
Normal file
189
cargos/gemini-sdk/README.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# Gemini SDK
|
||||
|
||||
A simple and powerful Rust SDK for Google Gemini AI services.
|
||||
|
||||
## Features
|
||||
|
||||
- **Simple Interface**: Send text + attachments to Gemini AI
|
||||
- **Agent System**: Create specialized AI agents with custom system prompts and configurations
|
||||
- **Type Safety**: Strong typing with compile-time error checking
|
||||
- **Easy Integration**: Minimal external dependencies
|
||||
|
||||
## Quick Start
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
gemini-sdk = { path = "../gemini-sdk" }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```rust
|
||||
use gemini_sdk::{GeminiSDK, MessageRequest, Agent};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize SDK
|
||||
let mut sdk = GeminiSDK::new()?;
|
||||
|
||||
// Simple text message
|
||||
let request = MessageRequest::text_only("Hello, how are you?");
|
||||
let response = sdk.send_message(request).await?;
|
||||
println!("Response: {}", response.content);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Using Agents
|
||||
|
||||
```rust
|
||||
use gemini_sdk::{GeminiSDK, MessageRequest, Agent};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut sdk = GeminiSDK::new()?;
|
||||
|
||||
// Create a specialized agent
|
||||
let agent = Agent::new(
|
||||
"code_reviewer",
|
||||
"Code Reviewer",
|
||||
"You are an expert code reviewer. Analyze code for bugs, suggest improvements, and provide constructive feedback."
|
||||
).with_temperature(0.3) // Lower temperature for more focused responses
|
||||
.with_max_tokens(4096);
|
||||
|
||||
sdk.add_agent(agent);
|
||||
|
||||
// Use the agent
|
||||
let request = MessageRequest::with_agent(
|
||||
"Please review this Python code: def fibonacci(n): return n if n <= 1 else fibonacci(n-1) + fibonacci(n-2)",
|
||||
"code_reviewer"
|
||||
);
|
||||
|
||||
let response = sdk.send_message(request).await?;
|
||||
println!("Code review: {}", response.content);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### With Image Attachments
|
||||
|
||||
```rust
|
||||
use gemini_sdk::{GeminiSDK, MessageRequest, Attachment};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut sdk = GeminiSDK::new()?;
|
||||
|
||||
let request = MessageRequest::with_attachments(
|
||||
"What do you see in this image?",
|
||||
vec![Attachment::new("./image.jpg")]
|
||||
);
|
||||
|
||||
let response = sdk.send_message(request).await?;
|
||||
println!("Image analysis: {}", response.content);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The SDK uses environment variables for configuration, with sensible defaults:
|
||||
|
||||
- `GEMINI_BASE_URL`: API base URL (default: Bowong AI endpoint)
|
||||
- `GEMINI_BEARER_TOKEN`: Bearer token for authentication (default: "bowong7777")
|
||||
- `GEMINI_MODEL_NAME`: Model to use (default: "gemini-2.5-flash")
|
||||
- `GEMINI_TEMPERATURE`: Default temperature (default: 0.7)
|
||||
- `GEMINI_MAX_TOKENS`: Default max tokens (default: 64000)
|
||||
|
||||
You can also create a custom configuration:
|
||||
|
||||
```rust
|
||||
use gemini_sdk::{GeminiSDK, GeminiConfig};
|
||||
|
||||
let config = GeminiConfig {
|
||||
base_url: "https://your-api-endpoint.com".to_string(),
|
||||
bearer_token: "your-token".to_string(),
|
||||
model_name: "gemini-2.5-flash".to_string(),
|
||||
temperature: 0.8,
|
||||
max_tokens: 32000,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut sdk = GeminiSDK::with_config(config)?;
|
||||
```
|
||||
|
||||
## Agent System
|
||||
|
||||
Agents are specialized AI assistants with their own system prompts and configurations:
|
||||
|
||||
```rust
|
||||
// Create an agent
|
||||
let agent = Agent::new("agent_id", "Agent Name", "System prompt here")
|
||||
.with_temperature(0.5) // 0.0 = focused, 2.0 = creative
|
||||
.with_max_tokens(8192); // Maximum response length
|
||||
|
||||
// Add to SDK
|
||||
sdk.add_agent(agent);
|
||||
|
||||
// Use the agent
|
||||
let request = MessageRequest::with_agent("Your message", "agent_id");
|
||||
let response = sdk.send_message(request).await?;
|
||||
```
|
||||
|
||||
### Agent Management
|
||||
|
||||
```rust
|
||||
// List all agents
|
||||
let agents = sdk.list_agents();
|
||||
for agent in agents {
|
||||
println!("Agent: {} ({})", agent.name, agent.id);
|
||||
}
|
||||
|
||||
// Get specific agent
|
||||
if let Some(agent) = sdk.get_agent("agent_id") {
|
||||
println!("Found agent: {}", agent.name);
|
||||
}
|
||||
|
||||
// Remove agent
|
||||
sdk.remove_agent("agent_id");
|
||||
```
|
||||
|
||||
## Supported File Types
|
||||
|
||||
- **Images**: JPEG, PNG, GIF, WebP, BMP
|
||||
- **Videos**: MP4, AVI, MOV, WebM (uploaded to cloud storage)
|
||||
- **Text**: Plain text, Markdown, HTML, JSON
|
||||
|
||||
## Error Handling
|
||||
|
||||
The SDK provides detailed error types:
|
||||
|
||||
```rust
|
||||
use gemini_sdk::SDKError;
|
||||
|
||||
match sdk.send_message(request).await {
|
||||
Ok(response) => println!("Success: {}", response.content),
|
||||
Err(SDKError::AgentNotFound(id)) => println!("Agent {} not found", id),
|
||||
Err(SDKError::Attachment(msg)) => println!("Attachment error: {}", msg),
|
||||
Err(SDKError::Api { status, message }) => println!("API error {}: {}", status, message),
|
||||
Err(e) => println!("Other error: {}", e),
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Run the examples to see the SDK in action:
|
||||
|
||||
```bash
|
||||
cargo run --example basic_usage
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
119
cargos/gemini-sdk/examples/basic_usage.rs
Normal file
119
cargos/gemini-sdk/examples/basic_usage.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
//! Basic usage example for the Gemini SDK
|
||||
|
||||
use gemini_sdk::{GeminiSDK, MessageRequest, Agent};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize SDK
|
||||
let mut sdk = GeminiSDK::new()?;
|
||||
|
||||
// Example 1: Simple text message
|
||||
println!("=== Example 1: Simple text message ===");
|
||||
let request = MessageRequest::text_only("Hello, how are you?");
|
||||
|
||||
match sdk.send_message(request).await {
|
||||
Ok(response) => {
|
||||
println!("Response: {}", response.content);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Example 2: Using an agent
|
||||
println!("\n=== Example 2: Using an agent ===");
|
||||
let agent = Agent::new(
|
||||
"helpful_assistant",
|
||||
"Helpful Assistant",
|
||||
"You are a helpful assistant that provides clear and concise answers."
|
||||
).with_temperature(0.7);
|
||||
|
||||
sdk.add_agent(agent);
|
||||
|
||||
let request = MessageRequest::with_agent(
|
||||
"Explain quantum computing in simple terms",
|
||||
"helpful_assistant"
|
||||
);
|
||||
|
||||
match sdk.send_message(request).await {
|
||||
Ok(response) => {
|
||||
println!("Agent response: {}", response.content);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Example 3: Message with image attachment (commented out as it requires a real image file)
|
||||
/*
|
||||
println!("\n=== Example 3: Message with image attachment ===");
|
||||
let request = MessageRequest::with_attachments(
|
||||
"What do you see in this image?",
|
||||
vec![Attachment::new("./example.jpg")]
|
||||
);
|
||||
|
||||
match sdk.send_message(request).await {
|
||||
Ok(response) => {
|
||||
println!("Image analysis: {}", response.content);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Example 4: Creating a specialized agent
|
||||
println!("\n=== Example 4: Creating a specialized agent ===");
|
||||
let code_agent = Agent::new(
|
||||
"code_reviewer",
|
||||
"Code Reviewer",
|
||||
r#"You are an expert code reviewer. When given code, you should:
|
||||
1. Analyze the code for potential bugs or issues
|
||||
2. Suggest improvements for readability and performance
|
||||
3. Check for best practices and coding standards
|
||||
4. Provide constructive feedback
|
||||
|
||||
Always be specific and provide examples when possible."#
|
||||
).with_temperature(0.3) // Lower temperature for more focused responses
|
||||
.with_max_tokens(4096);
|
||||
|
||||
sdk.add_agent(code_agent);
|
||||
|
||||
let code_example = r#"
|
||||
def fibonacci(n):
|
||||
if n <= 1:
|
||||
return n
|
||||
else:
|
||||
return fibonacci(n-1) + fibonacci(n-2)
|
||||
|
||||
print(fibonacci(10))
|
||||
"#;
|
||||
|
||||
let request = MessageRequest::with_agent(
|
||||
format!("Please review this Python code:\n\n```python{}\n```", code_example),
|
||||
"code_reviewer"
|
||||
);
|
||||
|
||||
match sdk.send_message(request).await {
|
||||
Ok(response) => {
|
||||
println!("Code review: {}", response.content);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Example 5: List all agents
|
||||
println!("\n=== Example 5: List all agents ===");
|
||||
let agents = sdk.list_agents();
|
||||
for agent in agents {
|
||||
println!("Agent: {} (ID: {})", agent.name, agent.id);
|
||||
println!(" Temperature: {}", agent.temperature);
|
||||
println!(" Max tokens: {}", agent.max_tokens);
|
||||
println!(" System prompt: {}...",
|
||||
agent.system_prompt.chars().take(50).collect::<String>());
|
||||
println!();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
477
cargos/gemini-sdk/promptx.md
Normal file
477
cargos/gemini-sdk/promptx.md
Normal file
@@ -0,0 +1,477 @@
|
||||
# Gemini SDK 开发提示词
|
||||
|
||||
## 项目概述
|
||||
|
||||
基于 `apps\desktop\src-tauri\src\infrastructure\gemini_service.rs` 的现有实现,创建一个独立的 Rust crate `gemini-sdk`,提供简洁易用的 Gemini AI 服务接口。
|
||||
|
||||
## 核心设计目标
|
||||
|
||||
1. **简化接口**: 提供统一的消息发送接口,支持文本+附件输入
|
||||
2. **Agent 系统**: 支持多个 AI Agent,每个 Agent 有独立的 system prompt 和配置
|
||||
3. **多轮对话**: 支持会话管理和历史记录
|
||||
4. **类型安全**: 强类型设计,编译时错误检查
|
||||
5. **易于集成**: 最小化外部依赖,易于在其他项目中使用
|
||||
|
||||
## 技术架构
|
||||
|
||||
### 依赖管理
|
||||
```toml
|
||||
[dependencies]
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
reqwest = { version = "0.11", features = ["json", "multipart"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
anyhow = "1.0"
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
base64 = "0.22"
|
||||
rusqlite = { version = "0.31", features = ["bundled", "chrono"] }
|
||||
```
|
||||
|
||||
### 核心数据结构
|
||||
|
||||
```rust
|
||||
// SDK 主体
|
||||
pub struct GeminiSDK {
|
||||
service: GeminiService,
|
||||
conversation_repo: Option<Arc<ConversationRepository>>,
|
||||
agents: HashMap<String, Agent>,
|
||||
}
|
||||
|
||||
// Agent 定义
|
||||
pub struct Agent {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub system_prompt: String,
|
||||
pub config: AgentConfig,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub struct AgentConfig {
|
||||
pub temperature: f32,
|
||||
pub max_tokens: u32,
|
||||
pub timeout: u64,
|
||||
pub enable_conversation: bool,
|
||||
}
|
||||
|
||||
// 消息结构
|
||||
pub struct MessageRequest {
|
||||
pub text: String,
|
||||
pub attachments: Vec<Attachment>,
|
||||
pub agent_id: Option<String>,
|
||||
pub session_id: Option<String>,
|
||||
pub config: Option<MessageConfig>,
|
||||
}
|
||||
|
||||
pub struct MessageResponse {
|
||||
pub content: String,
|
||||
pub session_id: String,
|
||||
pub message_id: Option<String>,
|
||||
pub agent_id: Option<String>,
|
||||
}
|
||||
|
||||
// 附件定义
|
||||
pub struct Attachment {
|
||||
pub path: String,
|
||||
pub mime_type: Option<String>,
|
||||
}
|
||||
|
||||
// 会话管理
|
||||
pub struct ConversationSession {
|
||||
pub session_id: String,
|
||||
pub title: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub struct HistoryMessage {
|
||||
pub role: MessageRole,
|
||||
pub content: String,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub attachments: Vec<String>,
|
||||
}
|
||||
```
|
||||
|
||||
## 核心功能接口
|
||||
|
||||
### 1. SDK 初始化
|
||||
```rust
|
||||
impl GeminiSDK {
|
||||
// 基础初始化
|
||||
pub fn new() -> Result<Self>;
|
||||
|
||||
// 支持多轮对话
|
||||
pub fn with_conversation_support(db_path: Option<String>) -> Result<Self>;
|
||||
|
||||
// 自定义配置
|
||||
pub fn with_config(config: GeminiConfig) -> Result<Self>;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 核心消息接口
|
||||
```rust
|
||||
impl GeminiSDK {
|
||||
// 主要方法:发送消息+附件,获得回复
|
||||
pub async fn send_message(&mut self, request: MessageRequest) -> Result<MessageResponse>;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Agent 管理
|
||||
```rust
|
||||
impl GeminiSDK {
|
||||
// Agent CRUD
|
||||
pub fn create_agent(&mut self, name: String, system_prompt: String, config: Option<AgentConfig>) -> Result<Agent>;
|
||||
pub fn get_agent(&self, agent_id: &str) -> Option<&Agent>;
|
||||
pub fn list_agents(&self) -> Vec<&Agent>;
|
||||
pub fn update_agent(&mut self, agent_id: &str, name: Option<String>, system_prompt: Option<String>, config: Option<AgentConfig>) -> Result<()>;
|
||||
pub fn delete_agent(&mut self, agent_id: &str) -> Result<()>;
|
||||
|
||||
// 预设 Agent
|
||||
pub fn create_outfit_advisor_agent(&mut self) -> Result<Agent>;
|
||||
pub fn create_video_analyst_agent(&mut self) -> Result<Agent>;
|
||||
pub fn create_general_assistant_agent(&mut self) -> Result<Agent>;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 会话管理
|
||||
```rust
|
||||
impl GeminiSDK {
|
||||
// 会话 CRUD
|
||||
pub async fn create_session(&self, title: Option<String>) -> Result<ConversationSession>;
|
||||
pub async fn get_session(&self, session_id: &str) -> Result<Option<ConversationSession>>;
|
||||
pub async fn list_sessions(&self) -> Result<Vec<ConversationSession>>;
|
||||
pub async fn delete_session(&self, session_id: &str) -> Result<()>;
|
||||
|
||||
// 历史记录
|
||||
pub async fn get_conversation_history(&self, session_id: &str, limit: Option<u32>) -> Result<Vec<HistoryMessage>>;
|
||||
pub async fn clear_conversation_history(&self, session_id: &str) -> Result<()>;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 便捷构造函数
|
||||
```rust
|
||||
impl Attachment {
|
||||
pub fn new(path: impl Into<String>) -> Self;
|
||||
pub fn with_mime_type(path: impl Into<String>, mime_type: impl Into<String>) -> Self;
|
||||
}
|
||||
|
||||
impl MessageRequest {
|
||||
pub fn text_only(text: impl Into<String>) -> Self;
|
||||
pub fn with_attachments(text: impl Into<String>, attachments: Vec<Attachment>) -> Self;
|
||||
pub fn with_agent(text: impl Into<String>, agent_id: impl Into<String>) -> Self;
|
||||
}
|
||||
```
|
||||
|
||||
## 实现要求
|
||||
|
||||
### 1. 文件结构
|
||||
```
|
||||
cargos/gemini-sdk/
|
||||
├── Cargo.toml
|
||||
├── src/
|
||||
│ ├── lib.rs # 主入口
|
||||
│ ├── sdk.rs # SDK 主体实现
|
||||
│ ├── agent.rs # Agent 相关
|
||||
│ ├── conversation.rs # 会话管理
|
||||
│ ├── attachment.rs # 附件处理
|
||||
│ ├── error.rs # 错误定义
|
||||
│ └── presets/ # 预设 Agent
|
||||
│ ├── mod.rs
|
||||
│ ├── outfit_advisor.rs
|
||||
│ ├── video_analyst.rs
|
||||
│ └── general_assistant.rs
|
||||
├── examples/
|
||||
│ ├── basic_usage.rs
|
||||
│ ├── agent_demo.rs
|
||||
│ └── conversation_demo.rs
|
||||
└── tests/
|
||||
├── integration_tests.rs
|
||||
└── agent_tests.rs
|
||||
```
|
||||
|
||||
### 2. 核心实现逻辑
|
||||
|
||||
#### 消息发送流程
|
||||
1. 检查是否指定 Agent,获取对应配置和 system prompt
|
||||
2. 处理附件:根据文件类型选择上传(视频)或编码(图片)
|
||||
3. 构建 Gemini API 请求,包含 system prompt
|
||||
4. 如果启用会话,使用多轮对话模式;否则使用单轮模式
|
||||
5. 调用底层 GeminiService 方法
|
||||
6. 返回格式化的响应
|
||||
|
||||
#### Agent 系统
|
||||
1. 内存中维护 Agent 映射表
|
||||
2. 每个 Agent 包含独立的 system prompt 和配置
|
||||
3. 提供预设的专业 Agent(穿搭顾问、视频分析师等)
|
||||
4. 支持动态创建和管理自定义 Agent
|
||||
|
||||
#### 会话管理
|
||||
1. 可选的 SQLite 数据库支持
|
||||
2. 自动会话创建和历史记录保存
|
||||
3. 支持会话标题和元数据管理
|
||||
4. 历史消息检索和清理
|
||||
|
||||
### 3. 错误处理
|
||||
```rust
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SDKError {
|
||||
#[error("配置错误: {0}")]
|
||||
Config(String),
|
||||
|
||||
#[error("Agent 不存在: {0}")]
|
||||
AgentNotFound(String),
|
||||
|
||||
#[error("会话错误: {0}")]
|
||||
Session(String),
|
||||
|
||||
#[error("附件处理错误: {0}")]
|
||||
Attachment(String),
|
||||
|
||||
#[error("网络错误: {0}")]
|
||||
Network(#[from] reqwest::Error),
|
||||
|
||||
#[error("数据库错误: {0}")]
|
||||
Database(#[from] rusqlite::Error),
|
||||
|
||||
#[error("Gemini 服务错误: {0}")]
|
||||
GeminiService(#[from] anyhow::Error),
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 使用示例
|
||||
```rust
|
||||
// 基础使用
|
||||
let mut sdk = GeminiSDK::with_conversation_support(None)?;
|
||||
|
||||
// 创建穿搭顾问 Agent
|
||||
let agent = sdk.create_outfit_advisor_agent()?;
|
||||
|
||||
// 发送消息
|
||||
let request = MessageRequest {
|
||||
text: "分析这张穿搭图片".to_string(),
|
||||
attachments: vec![Attachment::new("./outfit.jpg")],
|
||||
agent_id: Some(agent.id),
|
||||
session_id: None,
|
||||
config: None,
|
||||
};
|
||||
|
||||
let response = sdk.send_message(request).await?;
|
||||
println!("回复: {}", response.content);
|
||||
```
|
||||
|
||||
## 开发指导
|
||||
|
||||
### 1. 代码复用策略
|
||||
- 直接复用 `gemini_service.rs` 中的核心逻辑
|
||||
- 提取通用的配置、错误处理和网络请求代码
|
||||
- 简化接口,隐藏内部复杂性
|
||||
|
||||
### 2. 测试策略
|
||||
- 单元测试:各个组件的独立功能
|
||||
- 集成测试:完整的消息发送流程
|
||||
- 示例代码:展示各种使用场景
|
||||
|
||||
### 3. 文档要求
|
||||
- 完整的 API 文档
|
||||
- 使用示例和最佳实践
|
||||
- 错误处理指南
|
||||
- 性能优化建议
|
||||
|
||||
### 4. 性能考虑
|
||||
- 连接池复用
|
||||
- 合理的超时设置
|
||||
- 内存使用优化
|
||||
- 异步操作支持
|
||||
|
||||
## 交付标准
|
||||
|
||||
1. **功能完整性**: 所有设计的接口都能正常工作
|
||||
2. **代码质量**: 遵循 Rust 最佳实践,通过 clippy 检查
|
||||
3. **测试覆盖**: 核心功能有完整的测试覆盖
|
||||
4. **文档完善**: 清晰的 API 文档和使用示例
|
||||
5. **易用性**: 简洁的接口,最小化学习成本
|
||||
|
||||
## 详细实现指南
|
||||
|
||||
### 1. 从现有代码迁移的关键组件
|
||||
|
||||
#### 需要复用的核心结构
|
||||
```rust
|
||||
// 从 gemini_service.rs 复用
|
||||
pub struct GeminiConfig { /* 保持原有字段 */ }
|
||||
pub struct GenerateContentRequest { /* 保持原有结构 */ }
|
||||
pub struct ContentPart { /* 保持原有结构 */ }
|
||||
pub enum Part { /* 保持原有枚举 */ }
|
||||
pub struct GenerationConfig { /* 保持原有结构 */ }
|
||||
```
|
||||
|
||||
#### 需要复用的核心方法
|
||||
- `get_access_token()` - 访问令牌管理
|
||||
- `upload_video_file()` - 视频文件上传
|
||||
- `generate_content_with_request()` - 内容生成
|
||||
- `query_llm_with_grounding_multi_turn()` - 多轮对话
|
||||
- `extract_json_from_response()` - JSON 解析
|
||||
|
||||
### 2. 简化的内部服务层
|
||||
|
||||
```rust
|
||||
// 简化的内部服务,封装复杂逻辑
|
||||
struct InternalGeminiService {
|
||||
config: GeminiConfig,
|
||||
client: reqwest::Client,
|
||||
access_token: Option<String>,
|
||||
token_expires_at: Option<u64>,
|
||||
}
|
||||
|
||||
impl InternalGeminiService {
|
||||
// 复用原有的核心方法,但简化接口
|
||||
async fn send_request(&mut self, parts: Vec<Part>, config: GenerationConfig, system_prompt: Option<String>) -> Result<String>;
|
||||
async fn upload_file(&mut self, path: &str) -> Result<String>;
|
||||
async fn send_multipart_request(&mut self, parts: Vec<Part>, config: GenerationConfig, system_prompt: Option<String>, session_id: Option<String>) -> Result<String>;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 错误恢复和重试机制
|
||||
|
||||
```rust
|
||||
impl GeminiSDK {
|
||||
async fn send_message_with_retry(&mut self, request: MessageRequest, max_retries: u32) -> Result<MessageResponse> {
|
||||
let mut last_error = None;
|
||||
|
||||
for attempt in 0..max_retries {
|
||||
match self.send_message_internal(request.clone()).await {
|
||||
Ok(response) => return Ok(response),
|
||||
Err(e) => {
|
||||
last_error = Some(e);
|
||||
if attempt < max_retries - 1 {
|
||||
let delay = std::time::Duration::from_secs(2_u64.pow(attempt));
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_error.unwrap())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 配置管理
|
||||
|
||||
```rust
|
||||
impl GeminiSDK {
|
||||
pub fn from_env() -> Result<Self> {
|
||||
let config = GeminiConfig {
|
||||
base_url: std::env::var("GEMINI_BASE_URL").unwrap_or_else(|_| "https://bowongai-dev--bowong-ai-video-gemini-fastapi-webapp.modal.run".to_string()),
|
||||
bearer_token: std::env::var("GEMINI_BEARER_TOKEN").unwrap_or_else(|_| "bowong7777".to_string()),
|
||||
// ... 其他配置
|
||||
};
|
||||
|
||||
Self::with_config(config)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 完整的使用示例
|
||||
|
||||
```rust
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 1. 初始化 SDK
|
||||
let mut sdk = GeminiSDK::with_conversation_support(None)?;
|
||||
|
||||
|
||||
// 3. 穿搭分析示例
|
||||
let outfit_request = MessageRequest {
|
||||
text: "请分析这张穿搭图片,给出专业的搭配建议".to_string(),
|
||||
attachments: vec![Attachment::new("./examples/outfit.jpg")],
|
||||
agent_id: Some(outfit_agent.id.clone()),
|
||||
session_id: None,
|
||||
config: None,
|
||||
};
|
||||
|
||||
let outfit_response = sdk.send_message(outfit_request).await?;
|
||||
println!("穿搭顾问分析:\n{}", outfit_response.content);
|
||||
|
||||
// 4. 视频分析示例
|
||||
let video_request = MessageRequest {
|
||||
text: "请分析这个视频的内容质量和改进建议".to_string(),
|
||||
attachments: vec![Attachment::new("./examples/video.mp4")],
|
||||
agent_id: Some(video_agent.id),
|
||||
session_id: None,
|
||||
config: Some(MessageConfig {
|
||||
temperature: Some(0.6),
|
||||
max_tokens: Some(12000),
|
||||
timeout: Some(180),
|
||||
}),
|
||||
};
|
||||
|
||||
let video_response = sdk.send_message(video_request).await?;
|
||||
println!("视频分析报告:\n{}", video_response.content);
|
||||
|
||||
// 5. 多轮对话示例
|
||||
let session = sdk.create_session(Some("穿搭咨询".to_string())).await?;
|
||||
|
||||
let follow_up = MessageRequest {
|
||||
text: "基于刚才的分析,我想要更正式一些的搭配方案".to_string(),
|
||||
attachments: vec![],
|
||||
agent_id: Some(outfit_agent.id),
|
||||
session_id: Some(session.session_id.clone()),
|
||||
config: None,
|
||||
};
|
||||
|
||||
let follow_response = sdk.send_message(follow_up).await?;
|
||||
println!("后续建议:\n{}", follow_response.content);
|
||||
|
||||
// 6. 查看对话历史
|
||||
let history = sdk.get_conversation_history(&session.session_id, Some(10)).await?;
|
||||
for msg in history {
|
||||
println!("{}: {}", msg.role, msg.content);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 质量保证要求
|
||||
|
||||
### 1. 代码规范
|
||||
- 使用 `cargo fmt` 格式化代码
|
||||
- 通过 `cargo clippy` 静态检查
|
||||
- 添加适当的文档注释
|
||||
- 遵循 Rust 命名约定
|
||||
|
||||
### 2. 测试要求
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_basic_message_sending() {
|
||||
// 测试基础消息发送功能
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_agent_management() {
|
||||
// 测试 Agent 创建和管理
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_conversation_flow() {
|
||||
// 测试多轮对话流程
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_attachment_processing() {
|
||||
// 测试附件处理
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 文档要求
|
||||
- 每个公共接口都要有详细的文档注释
|
||||
- 提供完整的使用示例
|
||||
- 包含错误处理指南
|
||||
- 性能和最佳实践说明
|
||||
|
||||
请基于这个详细的设计方案和实现指南,创建一个高质量、易用、功能完整的 Gemini SDK crate。
|
||||
83
cargos/gemini-sdk/src/agent.rs
Normal file
83
cargos/gemini-sdk/src/agent.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
//! Simple agent system for the Gemini SDK
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A simple AI agent with a system prompt and configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Agent {
|
||||
/// Unique identifier for the agent
|
||||
pub id: String,
|
||||
/// Human-readable name for the agent
|
||||
pub name: String,
|
||||
/// System prompt that defines the agent's behavior
|
||||
pub system_prompt: String,
|
||||
/// Temperature for text generation (0.0 to 2.0)
|
||||
pub temperature: f32,
|
||||
/// Maximum number of tokens to generate
|
||||
pub max_tokens: u32,
|
||||
}
|
||||
|
||||
impl Agent {
|
||||
/// Create a new agent
|
||||
pub fn new(
|
||||
id: impl Into<String>,
|
||||
name: impl Into<String>,
|
||||
system_prompt: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
name: name.into(),
|
||||
system_prompt: system_prompt.into(),
|
||||
temperature: 0.7,
|
||||
max_tokens: 8192,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the temperature
|
||||
pub fn with_temperature(mut self, temperature: f32) -> Self {
|
||||
self.temperature = temperature.clamp(0.0, 2.0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the max tokens
|
||||
pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
|
||||
self.max_tokens = max_tokens;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_agent_creation() {
|
||||
let agent = Agent::new("test", "Test Agent", "You are a test agent");
|
||||
assert_eq!(agent.id, "test");
|
||||
assert_eq!(agent.name, "Test Agent");
|
||||
assert_eq!(agent.system_prompt, "You are a test agent");
|
||||
assert_eq!(agent.temperature, 0.7);
|
||||
assert_eq!(agent.max_tokens, 8192);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_agent_configuration() {
|
||||
let agent = Agent::new("test", "Test", "Test")
|
||||
.with_temperature(1.0)
|
||||
.with_max_tokens(4096);
|
||||
|
||||
assert_eq!(agent.temperature, 1.0);
|
||||
assert_eq!(agent.max_tokens, 4096);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_temperature_clamping() {
|
||||
let agent = Agent::new("test", "Test", "Test")
|
||||
.with_temperature(3.0); // Should be clamped to 2.0
|
||||
assert_eq!(agent.temperature, 2.0);
|
||||
|
||||
let agent = Agent::new("test", "Test", "Test")
|
||||
.with_temperature(-1.0); // Should be clamped to 0.0
|
||||
assert_eq!(agent.temperature, 0.0);
|
||||
}
|
||||
}
|
||||
265
cargos/gemini-sdk/src/attachment.rs
Normal file
265
cargos/gemini-sdk/src/attachment.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
//! Attachment handling for the Gemini SDK
|
||||
//!
|
||||
//! This module provides utilities for handling file attachments in messages,
|
||||
//! including automatic MIME type detection and file validation.
|
||||
|
||||
use std::path::Path;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{Result, SDKError};
|
||||
|
||||
/// Represents a file attachment for a message
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Attachment {
|
||||
/// Path to the file
|
||||
pub path: String,
|
||||
/// MIME type of the file (auto-detected if not provided)
|
||||
pub mime_type: Option<String>,
|
||||
}
|
||||
|
||||
impl Attachment {
|
||||
/// Create a new attachment with automatic MIME type detection
|
||||
pub fn new(path: impl Into<String>) -> Self {
|
||||
let path = path.into();
|
||||
let mime_type = detect_mime_type(&path);
|
||||
|
||||
Self {
|
||||
path,
|
||||
mime_type,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new attachment with explicit MIME type
|
||||
pub fn with_mime_type(path: impl Into<String>, mime_type: impl Into<String>) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
mime_type: Some(mime_type.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the file extension
|
||||
pub fn extension(&self) -> Option<&str> {
|
||||
Path::new(&self.path)
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
}
|
||||
|
||||
/// Get the file name
|
||||
pub fn file_name(&self) -> Option<&str> {
|
||||
Path::new(&self.path)
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
}
|
||||
|
||||
/// Get the effective MIME type (detected or provided)
|
||||
pub fn effective_mime_type(&self) -> Option<&str> {
|
||||
self.mime_type.as_deref()
|
||||
}
|
||||
|
||||
/// Check if this is an image attachment
|
||||
pub fn is_image(&self) -> bool {
|
||||
self.effective_mime_type()
|
||||
.map(|mime| mime.starts_with("image/"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Check if this is a video attachment
|
||||
pub fn is_video(&self) -> bool {
|
||||
self.effective_mime_type()
|
||||
.map(|mime| mime.starts_with("video/"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Check if this is an audio attachment
|
||||
pub fn is_audio(&self) -> bool {
|
||||
self.effective_mime_type()
|
||||
.map(|mime| mime.starts_with("audio/"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Check if this is a text attachment
|
||||
pub fn is_text(&self) -> bool {
|
||||
self.effective_mime_type()
|
||||
.map(|mime| mime.starts_with("text/"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Validate the attachment
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
// Check if file exists
|
||||
if !Path::new(&self.path).exists() {
|
||||
return Err(SDKError::attachment(format!(
|
||||
"File does not exist: {}",
|
||||
self.path
|
||||
)));
|
||||
}
|
||||
|
||||
// Check if it's a file (not a directory)
|
||||
if !Path::new(&self.path).is_file() {
|
||||
return Err(SDKError::attachment(format!(
|
||||
"Path is not a file: {}",
|
||||
self.path
|
||||
)));
|
||||
}
|
||||
|
||||
// Check file size (limit to 100MB)
|
||||
let metadata = std::fs::metadata(&self.path)
|
||||
.map_err(|e| SDKError::attachment(format!("Cannot read file metadata: {}", e)))?;
|
||||
|
||||
const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024; // 100MB
|
||||
if metadata.len() > MAX_FILE_SIZE {
|
||||
return Err(SDKError::attachment(format!(
|
||||
"File too large: {} bytes (max: {} bytes)",
|
||||
metadata.len(),
|
||||
MAX_FILE_SIZE
|
||||
)));
|
||||
}
|
||||
|
||||
// Validate MIME type if provided
|
||||
if let Some(mime_type) = &self.mime_type {
|
||||
if !is_supported_mime_type(mime_type) {
|
||||
return Err(SDKError::attachment(format!(
|
||||
"Unsupported MIME type: {}",
|
||||
mime_type
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get file size in bytes
|
||||
pub fn file_size(&self) -> Result<u64> {
|
||||
let metadata = std::fs::metadata(&self.path)
|
||||
.map_err(|e| SDKError::attachment(format!("Cannot read file metadata: {}", e)))?;
|
||||
Ok(metadata.len())
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect MIME type based on file extension
|
||||
fn detect_mime_type(path: &str) -> Option<String> {
|
||||
let extension = Path::new(path)
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.map(|ext| ext.to_lowercase())?;
|
||||
|
||||
let mime_type = match extension.as_str() {
|
||||
// Images
|
||||
"jpg" | "jpeg" => "image/jpeg",
|
||||
"png" => "image/png",
|
||||
"gif" => "image/gif",
|
||||
"webp" => "image/webp",
|
||||
"bmp" => "image/bmp",
|
||||
"svg" => "image/svg+xml",
|
||||
"ico" => "image/x-icon",
|
||||
|
||||
// Videos
|
||||
"mp4" => "video/mp4",
|
||||
"avi" => "video/x-msvideo",
|
||||
"mov" => "video/quicktime",
|
||||
"wmv" => "video/x-ms-wmv",
|
||||
"flv" => "video/x-flv",
|
||||
"webm" => "video/webm",
|
||||
"mkv" => "video/x-matroska",
|
||||
"m4v" => "video/x-m4v",
|
||||
|
||||
// Audio
|
||||
"mp3" => "audio/mpeg",
|
||||
"wav" => "audio/wav",
|
||||
"ogg" => "audio/ogg",
|
||||
"flac" => "audio/flac",
|
||||
"aac" => "audio/aac",
|
||||
"m4a" => "audio/mp4",
|
||||
|
||||
// Text
|
||||
"txt" => "text/plain",
|
||||
"md" => "text/markdown",
|
||||
"html" => "text/html",
|
||||
"css" => "text/css",
|
||||
"js" => "text/javascript",
|
||||
"json" => "application/json",
|
||||
"xml" => "text/xml",
|
||||
|
||||
// Documents
|
||||
"pdf" => "application/pdf",
|
||||
"doc" => "application/msword",
|
||||
"docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"xls" => "application/vnd.ms-excel",
|
||||
"xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"ppt" => "application/vnd.ms-powerpoint",
|
||||
"pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(mime_type.to_string())
|
||||
}
|
||||
|
||||
/// Check if a MIME type is supported by Gemini
|
||||
fn is_supported_mime_type(mime_type: &str) -> bool {
|
||||
matches!(
|
||||
mime_type,
|
||||
// Images
|
||||
"image/jpeg" | "image/png" | "image/gif" | "image/webp" | "image/bmp" |
|
||||
// Videos
|
||||
"video/mp4" | "video/x-msvideo" | "video/quicktime" | "video/webm" |
|
||||
// Audio
|
||||
"audio/mpeg" | "audio/wav" | "audio/ogg" | "audio/flac" |
|
||||
// Text
|
||||
"text/plain" | "text/markdown" | "text/html" | "application/json"
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_attachment_creation() {
|
||||
let attachment = Attachment::new("test.jpg");
|
||||
assert_eq!(attachment.path, "test.jpg");
|
||||
assert_eq!(attachment.effective_mime_type(), Some("image/jpeg"));
|
||||
assert!(attachment.is_image());
|
||||
assert!(!attachment.is_video());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attachment_with_mime_type() {
|
||||
let attachment = Attachment::with_mime_type("test.unknown", "image/jpeg");
|
||||
assert_eq!(attachment.effective_mime_type(), Some("image/jpeg"));
|
||||
assert!(attachment.is_image());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mime_type_detection() {
|
||||
assert_eq!(detect_mime_type("test.jpg"), Some("image/jpeg".to_string()));
|
||||
assert_eq!(detect_mime_type("test.png"), Some("image/png".to_string()));
|
||||
assert_eq!(detect_mime_type("test.mp4"), Some("video/mp4".to_string()));
|
||||
assert_eq!(detect_mime_type("test.mp3"), Some("audio/mpeg".to_string()));
|
||||
assert_eq!(detect_mime_type("test.txt"), Some("text/plain".to_string()));
|
||||
assert_eq!(detect_mime_type("test.unknown"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_properties() {
|
||||
let attachment = Attachment::new("path/to/test.jpg");
|
||||
assert_eq!(attachment.extension(), Some("jpg"));
|
||||
assert_eq!(attachment.file_name(), Some("test.jpg"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attachment_validation() {
|
||||
// Test with non-existent file
|
||||
let attachment = Attachment::new("non_existent_file.jpg");
|
||||
assert!(attachment.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_supported_mime_types() {
|
||||
assert!(is_supported_mime_type("image/jpeg"));
|
||||
assert!(is_supported_mime_type("video/mp4"));
|
||||
assert!(is_supported_mime_type("audio/mpeg"));
|
||||
assert!(is_supported_mime_type("text/plain"));
|
||||
assert!(!is_supported_mime_type("application/octet-stream"));
|
||||
}
|
||||
}
|
||||
185
cargos/gemini-sdk/src/error.rs
Normal file
185
cargos/gemini-sdk/src/error.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
//! Error types for the Gemini SDK
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Result type alias for the SDK
|
||||
pub type Result<T> = std::result::Result<T, SDKError>;
|
||||
|
||||
/// Main error type for the Gemini SDK
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SDKError {
|
||||
/// Configuration errors
|
||||
#[error("Configuration error: {0}")]
|
||||
Config(String),
|
||||
|
||||
/// Agent not found
|
||||
#[error("Agent not found: {0}")]
|
||||
AgentNotFound(String),
|
||||
|
||||
/// Session errors
|
||||
#[error("Session error: {0}")]
|
||||
Session(String),
|
||||
|
||||
/// Attachment processing errors
|
||||
#[error("Attachment processing error: {0}")]
|
||||
Attachment(String),
|
||||
|
||||
/// Network errors
|
||||
#[error("Network error: {0}")]
|
||||
Network(#[from] reqwest::Error),
|
||||
|
||||
|
||||
|
||||
/// JSON parsing errors
|
||||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
/// IO errors
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// Generic errors
|
||||
#[error("Generic error: {0}")]
|
||||
Generic(#[from] anyhow::Error),
|
||||
|
||||
/// API errors
|
||||
#[error("API error: {message} (status: {status})")]
|
||||
Api { status: u16, message: String },
|
||||
|
||||
/// Timeout errors
|
||||
#[error("Operation timed out: {0}")]
|
||||
Timeout(String),
|
||||
|
||||
/// Invalid input errors
|
||||
#[error("Invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
|
||||
/// Authentication errors
|
||||
#[error("Authentication error: {0}")]
|
||||
Authentication(String),
|
||||
|
||||
/// Rate limit errors
|
||||
#[error("Rate limit exceeded: {0}")]
|
||||
RateLimit(String),
|
||||
|
||||
/// Service unavailable errors
|
||||
#[error("Service unavailable: {0}")]
|
||||
ServiceUnavailable(String),
|
||||
}
|
||||
|
||||
impl SDKError {
|
||||
/// Create a new configuration error
|
||||
pub fn config(msg: impl Into<String>) -> Self {
|
||||
Self::Config(msg.into())
|
||||
}
|
||||
|
||||
/// Create a new agent not found error
|
||||
pub fn agent_not_found(agent_id: impl Into<String>) -> Self {
|
||||
Self::AgentNotFound(agent_id.into())
|
||||
}
|
||||
|
||||
/// Create a new session error
|
||||
pub fn session(msg: impl Into<String>) -> Self {
|
||||
Self::Session(msg.into())
|
||||
}
|
||||
|
||||
/// Create a new attachment error
|
||||
pub fn attachment(msg: impl Into<String>) -> Self {
|
||||
Self::Attachment(msg.into())
|
||||
}
|
||||
|
||||
/// Create a new API error
|
||||
pub fn api(status: u16, message: impl Into<String>) -> Self {
|
||||
Self::Api {
|
||||
status,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new timeout error
|
||||
pub fn timeout(msg: impl Into<String>) -> Self {
|
||||
Self::Timeout(msg.into())
|
||||
}
|
||||
|
||||
/// Create a new invalid input error
|
||||
pub fn invalid_input(msg: impl Into<String>) -> Self {
|
||||
Self::InvalidInput(msg.into())
|
||||
}
|
||||
|
||||
/// Create a new authentication error
|
||||
pub fn authentication(msg: impl Into<String>) -> Self {
|
||||
Self::Authentication(msg.into())
|
||||
}
|
||||
|
||||
/// Create a new rate limit error
|
||||
pub fn rate_limit(msg: impl Into<String>) -> Self {
|
||||
Self::RateLimit(msg.into())
|
||||
}
|
||||
|
||||
/// Create a new service unavailable error
|
||||
pub fn service_unavailable(msg: impl Into<String>) -> Self {
|
||||
Self::ServiceUnavailable(msg.into())
|
||||
}
|
||||
|
||||
/// Check if the error is retryable
|
||||
pub fn is_retryable(&self) -> bool {
|
||||
match self {
|
||||
Self::Network(_) => true,
|
||||
Self::Api { status, .. } => {
|
||||
// Retry on server errors (5xx) and some client errors
|
||||
*status >= 500 || *status == 429 || *status == 408
|
||||
}
|
||||
Self::Timeout(_) => true,
|
||||
Self::RateLimit(_) => true,
|
||||
Self::ServiceUnavailable(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the error category for logging/metrics
|
||||
pub fn category(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Config(_) => "config",
|
||||
Self::AgentNotFound(_) => "agent",
|
||||
Self::Session(_) => "session",
|
||||
Self::Attachment(_) => "attachment",
|
||||
Self::Network(_) => "network",
|
||||
Self::Json(_) => "json",
|
||||
Self::Io(_) => "io",
|
||||
Self::Generic(_) => "generic",
|
||||
Self::Api { .. } => "api",
|
||||
Self::Timeout(_) => "timeout",
|
||||
Self::InvalidInput(_) => "input",
|
||||
Self::Authentication(_) => "auth",
|
||||
Self::RateLimit(_) => "rate_limit",
|
||||
Self::ServiceUnavailable(_) => "service",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_error_creation() {
|
||||
let err = SDKError::config("test config error");
|
||||
assert!(matches!(err, SDKError::Config(_)));
|
||||
assert_eq!(err.to_string(), "Configuration error: test config error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_retryable() {
|
||||
assert!(SDKError::api(500, "server error").is_retryable());
|
||||
assert!(SDKError::api(429, "rate limit").is_retryable());
|
||||
assert!(!SDKError::api(400, "bad request").is_retryable());
|
||||
assert!(!SDKError::config("test").is_retryable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_category() {
|
||||
assert_eq!(SDKError::config("test").category(), "config");
|
||||
assert_eq!(SDKError::agent_not_found("test").category(), "agent");
|
||||
assert_eq!(SDKError::api(500, "test").category(), "api");
|
||||
}
|
||||
}
|
||||
166
cargos/gemini-sdk/src/lib.rs
Normal file
166
cargos/gemini-sdk/src/lib.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
//! # Gemini SDK
|
||||
//!
|
||||
//! A simple Rust SDK for Google Gemini AI services.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! - **Simple Interface**: Send text + attachments to Gemini AI
|
||||
//! - **Type Safety**: Strong typing with compile-time error checking
|
||||
//! - **Easy Integration**: Minimal external dependencies
|
||||
//!
|
||||
//! ## Quick Start
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use gemini_sdk::{GeminiSDK, MessageRequest, Attachment, Agent};
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! // Initialize SDK
|
||||
//! let mut sdk = GeminiSDK::new()?;
|
||||
//!
|
||||
//! // Create an agent (optional)
|
||||
//! let agent = Agent::new("assistant", "Assistant", "You are a helpful assistant");
|
||||
//! sdk.add_agent(agent);
|
||||
//!
|
||||
//! // Send a message with attachment
|
||||
//! let request = MessageRequest {
|
||||
//! text: "Analyze this image".to_string(),
|
||||
//! attachments: vec![Attachment::new("./image.jpg")],
|
||||
//! agent_id: Some("assistant".to_string()),
|
||||
//! };
|
||||
//!
|
||||
//! let response = sdk.send_message(request).await?;
|
||||
//! println!("Response: {}", response.content);
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub mod sdk;
|
||||
pub mod agent;
|
||||
pub mod attachment;
|
||||
pub mod error;
|
||||
|
||||
// Re-export main types for convenience
|
||||
pub use sdk::{GeminiSDK, MessageRequest, MessageResponse};
|
||||
pub use agent::Agent;
|
||||
pub use attachment::Attachment;
|
||||
pub use error::{SDKError, Result};
|
||||
|
||||
/// SDK version
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Default configuration for the SDK
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GeminiConfig {
|
||||
pub base_url: String,
|
||||
pub bearer_token: String,
|
||||
pub timeout: u64,
|
||||
pub model_name: String,
|
||||
pub max_retries: u32,
|
||||
pub retry_delay: u64,
|
||||
pub temperature: f32,
|
||||
pub max_tokens: u32,
|
||||
pub cloudflare_project_id: String,
|
||||
pub cloudflare_gateway_id: String,
|
||||
pub google_project_id: String,
|
||||
pub regions: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for GeminiConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_url: "https://bowongai-dev--bowong-ai-video-gemini-fastapi-webapp.modal.run".to_string(),
|
||||
bearer_token: "bowong7777".to_string(),
|
||||
timeout: 120,
|
||||
model_name: "gemini-2.5-pro".to_string(),
|
||||
max_retries: 3,
|
||||
retry_delay: 2,
|
||||
temperature: 0.7,
|
||||
max_tokens: 64000,
|
||||
cloudflare_project_id: "67720b647ff2b55cf37ba3ef9e677083".to_string(),
|
||||
cloudflare_gateway_id: "bowong-dev".to_string(),
|
||||
google_project_id: "gen-lang-client-0413414134".to_string(),
|
||||
regions: vec![
|
||||
"us-central1".to_string(),
|
||||
"us-east1".to_string(),
|
||||
"europe-west1".to_string(),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GeminiConfig {
|
||||
/// Create configuration from environment variables
|
||||
pub fn from_env() -> Self {
|
||||
Self {
|
||||
base_url: std::env::var("GEMINI_BASE_URL")
|
||||
.unwrap_or_else(|_| Self::default().base_url),
|
||||
bearer_token: std::env::var("GEMINI_BEARER_TOKEN")
|
||||
.unwrap_or_else(|_| Self::default().bearer_token),
|
||||
timeout: std::env::var("GEMINI_TIMEOUT")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(Self::default().timeout),
|
||||
model_name: std::env::var("GEMINI_MODEL_NAME")
|
||||
.unwrap_or_else(|_| Self::default().model_name),
|
||||
max_retries: std::env::var("GEMINI_MAX_RETRIES")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(Self::default().max_retries),
|
||||
retry_delay: std::env::var("GEMINI_RETRY_DELAY")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(Self::default().retry_delay),
|
||||
temperature: std::env::var("GEMINI_TEMPERATURE")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(Self::default().temperature),
|
||||
max_tokens: std::env::var("GEMINI_MAX_TOKENS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(Self::default().max_tokens),
|
||||
cloudflare_project_id: std::env::var("GEMINI_CLOUDFLARE_PROJECT_ID")
|
||||
.unwrap_or_else(|_| Self::default().cloudflare_project_id),
|
||||
cloudflare_gateway_id: std::env::var("GEMINI_CLOUDFLARE_GATEWAY_ID")
|
||||
.unwrap_or_else(|_| Self::default().cloudflare_gateway_id),
|
||||
google_project_id: std::env::var("GEMINI_GOOGLE_PROJECT_ID")
|
||||
.unwrap_or_else(|_| Self::default().google_project_id),
|
||||
regions: std::env::var("GEMINI_REGIONS")
|
||||
.ok()
|
||||
.map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
|
||||
.unwrap_or_else(|| Self::default().regions),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_config_default() {
|
||||
let config = GeminiConfig::default();
|
||||
assert!(!config.base_url.is_empty());
|
||||
assert!(!config.bearer_token.is_empty());
|
||||
assert!(config.timeout > 0);
|
||||
assert!(!config.model_name.is_empty());
|
||||
assert!(config.max_retries > 0);
|
||||
assert!(config.temperature >= 0.0 && config.temperature <= 2.0);
|
||||
assert!(config.max_tokens > 0);
|
||||
assert!(!config.regions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_from_env() {
|
||||
// Test that from_env doesn't panic and returns valid config
|
||||
let config = GeminiConfig::from_env();
|
||||
assert!(!config.base_url.is_empty());
|
||||
assert!(!config.bearer_token.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
assert!(!VERSION.is_empty());
|
||||
}
|
||||
}
|
||||
540
cargos/gemini-sdk/src/sdk.rs
Normal file
540
cargos/gemini-sdk/src/sdk.rs
Normal file
@@ -0,0 +1,540 @@
|
||||
//! Main SDK implementation
|
||||
//!
|
||||
//! This module contains the core GeminiSDK struct and its implementation.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use base64::Engine;
|
||||
use reqwest::multipart;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::agent::Agent;
|
||||
use crate::attachment::Attachment;
|
||||
use crate::error::{Result, SDKError};
|
||||
use crate::GeminiConfig;
|
||||
|
||||
/// Main SDK struct for interacting with Gemini AI services
|
||||
pub struct GeminiSDK {
|
||||
config: GeminiConfig,
|
||||
client: reqwest::Client,
|
||||
agents: HashMap<String, Agent>,
|
||||
access_token: Option<String>,
|
||||
token_expires_at: Option<u64>,
|
||||
}
|
||||
|
||||
/// Request for sending a message
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MessageRequest {
|
||||
/// Text content of the message
|
||||
pub text: String,
|
||||
/// File attachments
|
||||
pub attachments: Vec<Attachment>,
|
||||
/// ID of the agent to use (optional)
|
||||
pub agent_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Response from sending a message
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MessageResponse {
|
||||
/// Generated content
|
||||
pub content: String,
|
||||
/// Agent ID that was used
|
||||
pub agent_id: Option<String>,
|
||||
}
|
||||
|
||||
impl MessageRequest {
|
||||
/// Create a text-only message request
|
||||
pub fn text_only(text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
attachments: Vec::new(),
|
||||
agent_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a message request with attachments
|
||||
pub fn with_attachments(text: impl Into<String>, attachments: Vec<Attachment>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
attachments,
|
||||
agent_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a message request with a specific agent
|
||||
pub fn with_agent(text: impl Into<String>, agent_id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
attachments: Vec::new(),
|
||||
agent_id: Some(agent_id.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the agent ID
|
||||
pub fn agent(mut self, agent_id: impl Into<String>) -> Self {
|
||||
self.agent_id = Some(agent_id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an attachment
|
||||
pub fn attach(mut self, attachment: Attachment) -> Self {
|
||||
self.attachments.push(attachment);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl GeminiSDK {
|
||||
/// Create a new SDK instance with default configuration
|
||||
pub fn new() -> Result<Self> {
|
||||
Self::with_config(GeminiConfig::default())
|
||||
}
|
||||
|
||||
/// Create a new SDK instance with custom configuration
|
||||
pub fn with_config(config: GeminiConfig) -> Result<Self> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(config.timeout))
|
||||
.build()
|
||||
.map_err(|e| SDKError::config(format!("Failed to create HTTP client: {}", e)))?;
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
client,
|
||||
agents: HashMap::new(),
|
||||
access_token: None,
|
||||
token_expires_at: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new SDK instance from environment variables
|
||||
pub fn from_env() -> Result<Self> {
|
||||
Self::with_config(GeminiConfig::from_env())
|
||||
}
|
||||
|
||||
/// Get the current configuration
|
||||
pub fn config(&self) -> &GeminiConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Update the configuration
|
||||
pub fn set_config(&mut self, config: GeminiConfig) {
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// Add an agent to the SDK
|
||||
pub fn add_agent(&mut self, agent: Agent) {
|
||||
self.agents.insert(agent.id.clone(), agent);
|
||||
}
|
||||
|
||||
/// Get an agent by ID
|
||||
pub fn get_agent(&self, agent_id: &str) -> Option<&Agent> {
|
||||
self.agents.get(agent_id)
|
||||
}
|
||||
|
||||
/// List all agents
|
||||
pub fn list_agents(&self) -> Vec<&Agent> {
|
||||
self.agents.values().collect()
|
||||
}
|
||||
|
||||
/// Remove an agent
|
||||
pub fn remove_agent(&mut self, agent_id: &str) -> Option<Agent> {
|
||||
self.agents.remove(agent_id)
|
||||
}
|
||||
|
||||
/// Send a message and get a response
|
||||
pub async fn send_message(&mut self, request: MessageRequest) -> Result<MessageResponse> {
|
||||
// Validate attachments
|
||||
for attachment in &request.attachments {
|
||||
attachment.validate()?;
|
||||
}
|
||||
|
||||
// Get agent configuration if specified
|
||||
let agent_config = if let Some(agent_id) = &request.agent_id {
|
||||
let agent = self.get_agent(agent_id)
|
||||
.ok_or_else(|| SDKError::agent_not_found(agent_id))?;
|
||||
Some((agent.temperature, agent.max_tokens, agent.system_prompt.clone()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Get access token
|
||||
let access_token = self.get_access_token().await?;
|
||||
|
||||
// Process attachments and build parts
|
||||
let mut parts = vec![Part::Text { text: request.text.clone() }];
|
||||
|
||||
for attachment in &request.attachments {
|
||||
if attachment.is_video() {
|
||||
// Upload video and get file URI
|
||||
let file_uri = self.upload_video_file(&attachment.path).await?;
|
||||
parts.push(Part::FileData {
|
||||
file_data: FileData {
|
||||
mime_type: attachment.effective_mime_type()
|
||||
.unwrap_or("video/mp4").to_string(),
|
||||
file_uri,
|
||||
}
|
||||
});
|
||||
} else if attachment.is_image() {
|
||||
// Encode image as base64
|
||||
let image_data = tokio::fs::read(&attachment.path).await
|
||||
.map_err(|e| SDKError::attachment(format!("Failed to read image: {}", e)))?;
|
||||
let base64_data = base64::prelude::BASE64_STANDARD.encode(&image_data);
|
||||
|
||||
parts.push(Part::InlineData {
|
||||
inline_data: InlineData {
|
||||
mime_type: attachment.effective_mime_type()
|
||||
.unwrap_or("image/jpeg").to_string(),
|
||||
data: base64_data,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return Err(SDKError::attachment(format!(
|
||||
"Unsupported attachment type: {:?}",
|
||||
attachment.effective_mime_type()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Build generation config
|
||||
let generation_config = GenerationConfig {
|
||||
temperature: agent_config.as_ref().map(|(temp, _, _)| *temp).unwrap_or(self.config.temperature),
|
||||
top_k: 32,
|
||||
top_p: 1.0,
|
||||
max_output_tokens: agent_config.as_ref().map(|(_, tokens, _)| *tokens).unwrap_or(self.config.max_tokens),
|
||||
};
|
||||
|
||||
// Build request with system prompt merged into user message if needed
|
||||
let user_text = if let Some((_, _, system_prompt)) = agent_config {
|
||||
format!("{}\n\nUser: {}", system_prompt, request.text)
|
||||
} else {
|
||||
request.text.clone()
|
||||
};
|
||||
|
||||
// Update the first text part with the combined message
|
||||
if let Some(Part::Text { text }) = parts.first_mut() {
|
||||
*text = user_text;
|
||||
}
|
||||
|
||||
let contents = vec![ContentPart {
|
||||
role: "user".to_string(),
|
||||
parts,
|
||||
}];
|
||||
|
||||
let request_data = GenerateContentRequest {
|
||||
contents,
|
||||
generation_config,
|
||||
};
|
||||
|
||||
// Send request
|
||||
let response_content = self.send_generate_request(&access_token, &request_data).await?;
|
||||
|
||||
Ok(MessageResponse {
|
||||
content: response_content,
|
||||
agent_id: request.agent_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get Google access token
|
||||
async fn get_access_token(&mut self) -> Result<String> {
|
||||
// Check cached token
|
||||
let current_time = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|e| SDKError::Generic(anyhow::anyhow!("System time error: {}", e)))?
|
||||
.as_secs();
|
||||
|
||||
if let (Some(token), Some(expires_at)) = (&self.access_token, self.token_expires_at) {
|
||||
if current_time < expires_at - 300 { // Refresh 5 minutes early
|
||||
return Ok(token.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Get new access token
|
||||
let url = format!("{}/google/access-token", self.config.base_url);
|
||||
|
||||
let response = self.client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.config.bearer_token))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
let error_body = response.text().await.unwrap_or_default();
|
||||
return Err(SDKError::api(status.as_u16(), format!("Failed to get access token: {}", error_body)));
|
||||
}
|
||||
|
||||
let response_text = response.text().await?;
|
||||
let token_response: TokenResponse = serde_json::from_str(&response_text)
|
||||
.map_err(|e| SDKError::Json(e))?;
|
||||
|
||||
// Cache token
|
||||
self.access_token = Some(token_response.access_token.clone());
|
||||
self.token_expires_at = Some(current_time + token_response.expires_in);
|
||||
|
||||
Ok(token_response.access_token)
|
||||
}
|
||||
|
||||
/// Upload video file to Gemini
|
||||
async fn upload_video_file(&mut self, video_path: &str) -> Result<String> {
|
||||
let access_token = self.get_access_token().await?;
|
||||
|
||||
// Read video file
|
||||
let video_data = tokio::fs::read(video_path).await
|
||||
.map_err(|e| SDKError::attachment(format!("Failed to read video file: {}", e)))?;
|
||||
|
||||
let file_name = std::path::Path::new(video_path)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("video.mp4");
|
||||
|
||||
// Create multipart form
|
||||
let form = multipart::Form::new()
|
||||
.part("file", multipart::Part::bytes(video_data)
|
||||
.file_name(file_name.to_string())
|
||||
.mime_str("video/mp4")
|
||||
.map_err(|e| SDKError::attachment(format!("Invalid MIME type: {}", e)))?);
|
||||
|
||||
// Upload to Vertex AI
|
||||
let upload_url = format!("{}/google/vertex-ai/upload", self.config.base_url);
|
||||
let query_params = [
|
||||
("bucket", "dy-media-storage"),
|
||||
("prefix", "video-analysis")
|
||||
];
|
||||
|
||||
let response = self.client
|
||||
.post(&upload_url)
|
||||
.header("Authorization", format!("Bearer {}", access_token))
|
||||
.header("x-google-api-key", &access_token)
|
||||
.query(&query_params)
|
||||
.multipart(form)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
return Err(SDKError::api(status.as_u16(), format!("Video upload failed: {}", error_text)));
|
||||
}
|
||||
|
||||
let response_text = response.text().await?;
|
||||
let upload_response: UploadResponse = serde_json::from_str(&response_text)
|
||||
.map_err(|e| SDKError::Json(e))?;
|
||||
|
||||
// Get file URI
|
||||
let file_uri = upload_response.urn
|
||||
.or(upload_response.file_uri)
|
||||
.or_else(|| upload_response.name.map(|name| format!("gs://dy-media-storage/{}", name)))
|
||||
.ok_or_else(|| SDKError::attachment("No file URI in upload response".to_string()))?;
|
||||
|
||||
Ok(file_uri)
|
||||
}
|
||||
|
||||
/// Send generate request to Gemini API
|
||||
async fn send_generate_request(
|
||||
&self,
|
||||
access_token: &str,
|
||||
request_data: &GenerateContentRequest,
|
||||
) -> Result<String> {
|
||||
let client_config = self.create_gemini_client(access_token);
|
||||
let generate_url = format!("{}/{}:generateContent", client_config.gateway_url, self.config.model_name);
|
||||
|
||||
let mut request_builder = self.client
|
||||
.post(&generate_url)
|
||||
.timeout(std::time::Duration::from_secs(self.config.timeout))
|
||||
.json(request_data);
|
||||
|
||||
// Add headers
|
||||
for (key, value) in &client_config.headers {
|
||||
request_builder = request_builder.header(key, value);
|
||||
}
|
||||
|
||||
let response = request_builder.send().await?;
|
||||
let status = response.status();
|
||||
|
||||
if !status.is_success() {
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
return Err(SDKError::api(status.as_u16(), format!("API request failed: {}", error_text)));
|
||||
}
|
||||
|
||||
let response_text = response.text().await?;
|
||||
let gemini_response: GeminiResponse = serde_json::from_str(&response_text)
|
||||
.map_err(|e| SDKError::Json(e))?;
|
||||
|
||||
if gemini_response.candidates.is_empty() {
|
||||
return Err(SDKError::api(200, "Empty response from API".to_string()));
|
||||
}
|
||||
|
||||
// Parse response content
|
||||
if let Some(candidate) = gemini_response.candidates.first() {
|
||||
if let Some(part) = candidate.content.parts.first() {
|
||||
return Ok(part.text.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Err(SDKError::api(200, "Invalid response format".to_string()))
|
||||
}
|
||||
|
||||
/// Create Gemini client configuration
|
||||
fn create_gemini_client(&self, access_token: &str) -> ClientConfig {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("Authorization".to_string(), format!("Bearer {}", access_token));
|
||||
headers.insert("Content-Type".to_string(), "application/json".to_string());
|
||||
|
||||
let region = self.config.regions.first()
|
||||
.unwrap_or(&"us-central1".to_string())
|
||||
.clone();
|
||||
|
||||
let gateway_url = format!(
|
||||
"https://gateway.ai.cloudflare.com/v1/{}/{}/google-vertex-ai/v1/projects/{}/locations/{}/publishers/google/models",
|
||||
self.config.cloudflare_project_id,
|
||||
self.config.cloudflare_gateway_id,
|
||||
self.config.google_project_id,
|
||||
region
|
||||
);
|
||||
|
||||
ClientConfig {
|
||||
gateway_url,
|
||||
headers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Data structures for Gemini API
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TokenResponse {
|
||||
access_token: String,
|
||||
expires_in: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ClientConfig {
|
||||
gateway_url: String,
|
||||
headers: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct UploadResponse {
|
||||
file_uri: Option<String>,
|
||||
urn: Option<String>,
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct GenerateContentRequest {
|
||||
contents: Vec<ContentPart>,
|
||||
#[serde(rename = "generationConfig")]
|
||||
generation_config: GenerationConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ContentPart {
|
||||
role: String,
|
||||
parts: Vec<Part>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
enum Part {
|
||||
Text { text: String },
|
||||
FileData {
|
||||
#[serde(rename = "fileData")]
|
||||
file_data: FileData
|
||||
},
|
||||
InlineData {
|
||||
#[serde(rename = "inlineData")]
|
||||
inline_data: InlineData
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct FileData {
|
||||
#[serde(rename = "mimeType")]
|
||||
mime_type: String,
|
||||
#[serde(rename = "fileUri")]
|
||||
file_uri: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct InlineData {
|
||||
#[serde(rename = "mimeType")]
|
||||
mime_type: String,
|
||||
data: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct GenerationConfig {
|
||||
temperature: f32,
|
||||
#[serde(rename = "topK")]
|
||||
top_k: u32,
|
||||
#[serde(rename = "topP")]
|
||||
top_p: f32,
|
||||
#[serde(rename = "maxOutputTokens")]
|
||||
max_output_tokens: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GeminiResponse {
|
||||
candidates: Vec<Candidate>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Candidate {
|
||||
content: Content,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Content {
|
||||
parts: Vec<ResponsePart>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ResponsePart {
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sdk_creation() {
|
||||
let sdk = GeminiSDK::new().unwrap();
|
||||
assert!(!sdk.config.base_url.is_empty());
|
||||
assert!(sdk.agents.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_request_builders() {
|
||||
let req = MessageRequest::text_only("Hello");
|
||||
assert_eq!(req.text, "Hello");
|
||||
assert!(req.attachments.is_empty());
|
||||
assert!(req.agent_id.is_none());
|
||||
|
||||
let req = MessageRequest::with_agent("Hello", "agent-1");
|
||||
assert_eq!(req.text, "Hello");
|
||||
assert_eq!(req.agent_id, Some("agent-1".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_agent_management() {
|
||||
let mut sdk = GeminiSDK::new().unwrap();
|
||||
|
||||
// Create agent
|
||||
let agent = Agent::new("test", "Test Agent", "You are a test agent");
|
||||
sdk.add_agent(agent.clone());
|
||||
|
||||
// Get agent
|
||||
let retrieved = sdk.get_agent(&agent.id);
|
||||
assert!(retrieved.is_some());
|
||||
assert_eq!(retrieved.unwrap().name, "Test Agent");
|
||||
|
||||
// List agents
|
||||
let agents = sdk.list_agents();
|
||||
assert_eq!(agents.len(), 1);
|
||||
|
||||
// Remove agent
|
||||
let removed = sdk.remove_agent(&agent.id);
|
||||
assert!(removed.is_some());
|
||||
assert!(sdk.get_agent(&agent.id).is_none());
|
||||
}
|
||||
}
|
||||
21
cargos/veo3-scene-writer/Cargo.toml
Normal file
21
cargos/veo3-scene-writer/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "veo3-scene-writer"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "VEO3 场景写作对话工具"
|
||||
|
||||
[dependencies]
|
||||
gemini-sdk = { path = "../gemini-sdk" }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
|
||||
[[example]]
|
||||
name = "simple_chat"
|
||||
path = "examples/simple_chat.rs"
|
||||
|
||||
[[example]]
|
||||
name = "attachment_demo"
|
||||
path = "examples/attachment_demo.rs"
|
||||
|
||||
[[example]]
|
||||
name = "daisy_demo"
|
||||
path = "examples/daisy_demo.rs"
|
||||
124
cargos/veo3-scene-writer/README.md
Normal file
124
cargos/veo3-scene-writer/README.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# VEO3 场景写作工具
|
||||
|
||||
基于 Gemini SDK 的 VEO3 场景写作对话工具,专门用于创建电影级的视频场景提示词。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🎬 **专业场景写作**: 使用 VEO3 专业提示词
|
||||
- 🤖 **智能对话**: 基于 Gemini AI 的对话式场景创建
|
||||
- 📸 **图片分析**: 支持上传图片进行角色分析
|
||||
- 🎥 **视频分析**: 支持视频文件分析和学习
|
||||
- 📄 **多格式输出**: 支持文本、JSON、YAML 格式
|
||||
- 🔄 **角色一致性**: 确保多个场景中角色特征保持一致
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 基本使用
|
||||
|
||||
```rust
|
||||
use veo3_scene_writer::Veo3SceneWriter;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut writer = Veo3SceneWriter::new().await?;
|
||||
let response = writer.send_message("你好,我想创建一个角色档案").await?;
|
||||
println!("回复: {}", response);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 带图片的使用
|
||||
|
||||
```rust
|
||||
let response = writer.send_message_with_attachment(
|
||||
"请分析这张图片中的角色",
|
||||
"character.jpg"
|
||||
).await?;
|
||||
```
|
||||
|
||||
## 演示示例
|
||||
|
||||
### 1. 基本对话演示
|
||||
```bash
|
||||
cargo run --example simple_chat
|
||||
```
|
||||
|
||||
### 2. 附件功能演示
|
||||
```bash
|
||||
cargo run --example attachment_demo
|
||||
```
|
||||
|
||||
### 3. Daisy 角色演示
|
||||
```bash
|
||||
# 需要先将图片文件 01.png 放在当前目录
|
||||
cargo run --example daisy_demo
|
||||
```
|
||||
|
||||
## Daisy 演示说明
|
||||
|
||||
`daisy_demo` 演示展示了完整的 VEO3 工作流程:
|
||||
|
||||
1. **准备工作**: 将 `01.png` 图片文件放在 `cargos/veo3-scene-writer/` 目录下
|
||||
2. **角色分析**: 基于图片为 "Daisy" 创建详细角色档案
|
||||
3. **场景创建**: 创建多个不同风格的 8 秒电影场景
|
||||
4. **格式输出**: 展示 JSON 格式的结构化输出
|
||||
|
||||
### 演示场景包括:
|
||||
- 🍵 **咖啡厅阅读场景**: 温馨静谧的室内场景
|
||||
- 🌳 **公园散步场景**: 自然光线的户外场景
|
||||
- 🌧️ **雨中奔跑场景**: 动感十足的动作场景
|
||||
|
||||
## 支持的文件格式
|
||||
|
||||
### 图片格式
|
||||
- JPG/JPEG
|
||||
- PNG
|
||||
- GIF
|
||||
- BMP
|
||||
- WebP
|
||||
|
||||
### 视频格式
|
||||
- MP4
|
||||
- AVI
|
||||
- MOV
|
||||
- MKV
|
||||
- WebM
|
||||
|
||||
## VEO3 工作流程
|
||||
|
||||
1. **Phase 1**: 角色档案导入
|
||||
- 定义角色的视觉特征
|
||||
- 设定角色的声音特征
|
||||
|
||||
2. **Phase 2**: 电影场景生成
|
||||
- 场景描述和角色动作
|
||||
- 摄影角度和运动
|
||||
- 灯光和氛围设置
|
||||
|
||||
3. **Phase 3**: 生成电影提示词
|
||||
- 输出 VEO3 优化的提示词
|
||||
- 支持多种格式输出
|
||||
|
||||
## 配置
|
||||
|
||||
SDK 会自动使用环境变量或默认配置。如需自定义配置:
|
||||
|
||||
```rust
|
||||
let config = gemini_sdk::GeminiConfig {
|
||||
base_url: "your-api-url".to_string(),
|
||||
bearer_token: "your-token".to_string(),
|
||||
// ... 其他配置
|
||||
};
|
||||
|
||||
let writer = Veo3SceneWriter::with_config(config).await?;
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 确保有有效的 Gemini API 配置
|
||||
- 图片和视频文件需要放在正确的路径
|
||||
- 大文件上传可能需要较长时间
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
BIN
cargos/veo3-scene-writer/examples/01.png
Normal file
BIN
cargos/veo3-scene-writer/examples/01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
33
cargos/veo3-scene-writer/examples/daisy_demo.rs
Normal file
33
cargos/veo3-scene-writer/examples/daisy_demo.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
//! Daisy 角色演示 - 使用图片创建 VEO3 场景
|
||||
|
||||
use veo3_scene_writer::Veo3SceneWriter;
|
||||
use std::path::Path;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// 创建 VEO3 场景写作工具
|
||||
let mut writer = Veo3SceneWriter::new().await?;
|
||||
|
||||
// 检查图片文件是否存在
|
||||
let image_path = "examples/01.png";
|
||||
if !Path::new(image_path).exists() {
|
||||
println!("❌ 错误: 找不到图片文件 {}", image_path);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let character_analysis = writer.send_message_with_attachment(
|
||||
"Daisy",
|
||||
image_path
|
||||
).await?;
|
||||
|
||||
println!("🤖 --------------------------------------- :\n{}\n", character_analysis);
|
||||
|
||||
let scene1 = writer.send_message(
|
||||
"first"
|
||||
).await?;
|
||||
|
||||
println!("🤖 --------------------------------------:\n{}\n", scene1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
60
cargos/veo3-scene-writer/scene-writer.md
Normal file
60
cargos/veo3-scene-writer/scene-writer.md
Normal file
@@ -0,0 +1,60 @@
|
||||
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
|
||||
425
cargos/veo3-scene-writer/src/lib.rs
Normal file
425
cargos/veo3-scene-writer/src/lib.rs
Normal file
@@ -0,0 +1,425 @@
|
||||
//! # VEO3 场景写作对话工具
|
||||
//!
|
||||
//! 基于 Gemini SDK 封装的简单对话工具,使用 VEO3 场景写作提示词
|
||||
//!
|
||||
//! ## 使用示例
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use veo3_scene_writer::Veo3SceneWriter;
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let mut writer = Veo3SceneWriter::new().await?;
|
||||
//! let response = writer.send_message("你好,我想创建一个角色档案").await?;
|
||||
//! println!("回复: {}", response);
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use gemini_sdk::{GeminiSDK, MessageRequest, Agent, Attachment};
|
||||
|
||||
/// VEO3 场景写作系统提示词
|
||||
const VEO3_SYSTEM_PROMPT: &str = r#"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"#;
|
||||
|
||||
/// VEO3 角色定义系统提示词 (待更新)
|
||||
const VEO3_ACTOR_DEFINE_PROMPT: &str = r#"Your Role: You are an expert cinematic video prompt writer, specializing in creating consistent characters profile for VEO3. Your primary goal is to guide me through a precise process to define a character with exact replication of visual details, and then generate detail character profile in JSON format. The user prefer reading Chinese instruction, but any data outcome and prompt for VEO3 must be in english.
|
||||
|
||||
Phase 1: Character Definition
|
||||
|
||||
Step 1: Character Origin
|
||||
|
||||
To begin, how do you want to define your consistent character?
|
||||
|
||||
Option A: Upload an Image: If you have a specific person in mind, upload an image. I will meticulously analyze every minute visual detail – from the shape of their eyes to the texture of their skin, the specific cut of their hair, age, makeup, tatoo and the body figure, skin tone, ignore their clothing detail – to create an exact replica-level, comprehensive, and precise description. If user also provide visual description prompt, I should also extract detail description from given prompt. If I need clarification on any aspect, I will ask targeted questions to ensure absolute accuracy before proceeding.
|
||||
|
||||
Option B: Generate a Person: If you don't have an image, just say "GENERATE PERSON." I'll then present you with 10 diverse and highly detailed character concepts, complete with a precise visual breakdown, for you to choose from.
|
||||
|
||||
Once we establish the visual foundation, we'll refine their voice.
|
||||
|
||||
Step 2: Character Refinement & Voice
|
||||
|
||||
We'll fine-tune your character's description and define their voice, aiming for an unwavering level of detail to ensure exact replication in VEO3.
|
||||
|
||||
Visual Refinement: We'll review and enhance the detailed description based on your feedback, ensuring every fine point is captured. This includes:Facial Features: Exact eye color and shape, distinct nose structure, lip fullness and shape, subtle wrinkles or lines (e.g., crow's feet, laugh lines), cheekbone prominence, jawline definition.
|
||||
|
||||
Hair: Precise color (e.g., "ash blonde," "dark auburn with subtle highlights"), exact style and cut (e.g., "shoulder-length wavy bob with a side part," "tightly coiled afro," "slicked-back with a widow's peak"), texture (e.g., "fine and wispy," "thick and coarse"), any flyaways or specific strands.
|
||||
|
||||
Skin: Skin tone (e.g., "fair with cool undertones," "warm olive"), presence of freckles, moles, scars, or distinct blemishes, skin texture (e.g., "smooth," "slightly textured," "weathered").
|
||||
|
||||
Build & Posture: Exact body type (e.g., "slender and athletic," "broad-shouldered," "petite"), typical posture (e.g., "erect and confident," "slightly hunched," "relaxed stance").
|
||||
|
||||
Clothing & Accessories: Specific garment types, fit (e.g., "tailored," "oversized," "form-fitting"), fabric textures (e.g., "crinkled linen," "soft cashmere," "distressed denim"), specific details like button types, stitching, pockets, jewelry (e.g., "delicate silver locket," "chunky gold signet ring"), eyeglasses style, any tattoos or piercings with their exact placement and design.
|
||||
|
||||
Demeanor & Subtle Expressions: Recurring micro-expressions, the "feel" of their presence (e.g., "quiet intensity," "approachable warmth," "reserved and observant").
|
||||
|
||||
Voice Profile: I'll offer voice options for you to select the desired tone, pitch, accent, and emotional quality. You can also describe a custom voice.
|
||||
|
||||
Once the character is defined export a complete profile of the character and its voice in JSON format."#;
|
||||
|
||||
/// VEO3 场景写作工具
|
||||
pub struct Veo3SceneWriter {
|
||||
sdk: GeminiSDK,
|
||||
agent_id: String,
|
||||
/// 会话历史记录
|
||||
conversation_history: Vec<String>,
|
||||
}
|
||||
|
||||
/// VEO3 角色定义工具
|
||||
pub struct Veo3ActorDefine {
|
||||
sdk: GeminiSDK,
|
||||
agent_id: String,
|
||||
/// 会话历史记录
|
||||
conversation_history: Vec<String>,
|
||||
}
|
||||
|
||||
impl Veo3SceneWriter {
|
||||
/// 创建新的 VEO3 场景写作实例
|
||||
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let mut sdk = GeminiSDK::new()?;
|
||||
|
||||
// 创建 VEO3 场景写作 agent
|
||||
let agent_id = "veo3-scene-writer";
|
||||
let agent = Agent::new(
|
||||
agent_id,
|
||||
"VEO3 场景写作专家",
|
||||
VEO3_SYSTEM_PROMPT
|
||||
)
|
||||
.with_temperature(0.7)
|
||||
.with_max_tokens(8192);
|
||||
|
||||
sdk.add_agent(agent);
|
||||
|
||||
Ok(Self {
|
||||
sdk,
|
||||
agent_id: agent_id.to_string(),
|
||||
conversation_history: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 使用自定义配置创建实例
|
||||
pub async fn with_config(config: gemini_sdk::GeminiConfig) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let mut sdk = GeminiSDK::with_config(config)?;
|
||||
|
||||
// 创建 VEO3 场景写作 agent
|
||||
let agent_id = "veo3-scene-writer";
|
||||
let agent = Agent::new(
|
||||
agent_id,
|
||||
"VEO3 场景写作专家",
|
||||
VEO3_SYSTEM_PROMPT
|
||||
)
|
||||
.with_temperature(0.7)
|
||||
.with_max_tokens(8192);
|
||||
|
||||
sdk.add_agent(agent);
|
||||
|
||||
Ok(Self {
|
||||
sdk,
|
||||
agent_id: agent_id.to_string(),
|
||||
conversation_history: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 发送文本消息
|
||||
pub async fn send_message(&mut self, message: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// 构建包含历史记录的完整消息
|
||||
let full_message = self.build_message_with_history(message);
|
||||
|
||||
let request = MessageRequest::with_agent(&full_message, &self.agent_id);
|
||||
let response = self.sdk.send_message(request).await?;
|
||||
|
||||
// 保存到历史记录
|
||||
self.add_to_history(message, &response.content);
|
||||
|
||||
Ok(response.content)
|
||||
}
|
||||
|
||||
/// 发送带附件的消息
|
||||
pub async fn send_message_with_attachments(
|
||||
&mut self,
|
||||
message: &str,
|
||||
attachment_paths: Vec<&str>
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// 构建包含历史记录的完整消息
|
||||
let full_message = self.build_message_with_history(message);
|
||||
|
||||
let mut request = MessageRequest::with_agent(&full_message, &self.agent_id);
|
||||
|
||||
for path in attachment_paths {
|
||||
request = request.attach(Attachment::new(path));
|
||||
}
|
||||
|
||||
let response = self.sdk.send_message(request).await?;
|
||||
|
||||
// 保存到历史记录
|
||||
self.add_to_history(message, &response.content);
|
||||
|
||||
Ok(response.content)
|
||||
}
|
||||
|
||||
/// 发送带单个附件的消息
|
||||
pub async fn send_message_with_attachment(
|
||||
&mut self,
|
||||
message: &str,
|
||||
attachment_path: &str
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
self.send_message_with_attachments(message, vec![attachment_path]).await
|
||||
}
|
||||
|
||||
/// 清除会话历史
|
||||
pub fn clear_conversation(&mut self) {
|
||||
self.conversation_history.clear();
|
||||
}
|
||||
|
||||
/// 获取会话历史
|
||||
pub fn get_conversation_history(&self) -> &Vec<String> {
|
||||
&self.conversation_history
|
||||
}
|
||||
|
||||
/// 构建包含历史记录的消息
|
||||
fn build_message_with_history(&self, message: &str) -> String {
|
||||
if self.conversation_history.is_empty() {
|
||||
message.to_string()
|
||||
} else {
|
||||
// 获取最近的几轮对话作为上下文
|
||||
let recent_history = self.conversation_history
|
||||
.iter()
|
||||
.rev()
|
||||
.take(6) // 最近3轮对话(用户+AI各3次)
|
||||
.rev()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
format!("对话历史:\n{}\n\n当前消息: {}", recent_history, message)
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加到历史记录
|
||||
fn add_to_history(&mut self, user_message: &str, ai_response: &str) {
|
||||
self.conversation_history.push(format!("用户: {}", user_message));
|
||||
self.conversation_history.push(format!("助手: {}", ai_response));
|
||||
|
||||
// 限制历史记录长度,保留最近20条消息
|
||||
if self.conversation_history.len() > 20 {
|
||||
self.conversation_history.drain(0..self.conversation_history.len() - 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_veo3_scene_writer_creation() {
|
||||
// 注意:这个测试需要有效的 API 配置才能通过
|
||||
// 在实际环境中运行
|
||||
if std::env::var("GEMINI_BEARER_TOKEN").is_ok() {
|
||||
let writer = Veo3SceneWriter::new().await;
|
||||
assert!(writer.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_veo3_actor_define_creation() {
|
||||
// 注意:这个测试需要有效的 API 配置才能通过
|
||||
// 在实际环境中运行
|
||||
if std::env::var("GEMINI_BEARER_TOKEN").is_ok() {
|
||||
let actor_define = Veo3ActorDefine::new().await;
|
||||
assert!(actor_define.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Veo3ActorDefine {
|
||||
/// 创建新的 VEO3 角色定义实例
|
||||
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let mut sdk = GeminiSDK::new()?;
|
||||
|
||||
// 创建 VEO3 角色定义 agent
|
||||
let agent_id = "veo3-actor-define";
|
||||
let agent = Agent::new(
|
||||
agent_id,
|
||||
"VEO3 角色定义专家",
|
||||
VEO3_ACTOR_DEFINE_PROMPT
|
||||
)
|
||||
.with_temperature(0.7)
|
||||
.with_max_tokens(8192);
|
||||
|
||||
sdk.add_agent(agent);
|
||||
|
||||
Ok(Self {
|
||||
sdk,
|
||||
agent_id: agent_id.to_string(),
|
||||
conversation_history: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 使用自定义配置创建实例
|
||||
pub async fn with_config(config: gemini_sdk::GeminiConfig) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let mut sdk = GeminiSDK::with_config(config)?;
|
||||
|
||||
// 创建 VEO3 角色定义 agent
|
||||
let agent_id = "veo3-actor-define";
|
||||
let agent = Agent::new(
|
||||
agent_id,
|
||||
"VEO3 角色定义专家",
|
||||
VEO3_ACTOR_DEFINE_PROMPT
|
||||
)
|
||||
.with_temperature(0.7)
|
||||
.with_max_tokens(8192);
|
||||
|
||||
sdk.add_agent(agent);
|
||||
|
||||
Ok(Self {
|
||||
sdk,
|
||||
agent_id: agent_id.to_string(),
|
||||
conversation_history: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 发送文本消息
|
||||
pub async fn send_message(&mut self, message: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// 构建包含历史记录的完整消息
|
||||
let full_message = self.build_message_with_history(message);
|
||||
|
||||
let request = MessageRequest::with_agent(&full_message, &self.agent_id);
|
||||
let response = self.sdk.send_message(request).await?;
|
||||
|
||||
// 保存到历史记录
|
||||
self.add_to_history(message, &response.content);
|
||||
|
||||
Ok(response.content)
|
||||
}
|
||||
|
||||
/// 发送带附件的消息
|
||||
pub async fn send_message_with_attachments(
|
||||
&mut self,
|
||||
message: &str,
|
||||
attachment_paths: Vec<&str>
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// 构建包含历史记录的完整消息
|
||||
let full_message = self.build_message_with_history(message);
|
||||
|
||||
let mut request = MessageRequest::with_agent(&full_message, &self.agent_id);
|
||||
|
||||
for path in attachment_paths {
|
||||
request = request.attach(Attachment::new(path));
|
||||
}
|
||||
|
||||
let response = self.sdk.send_message(request).await?;
|
||||
|
||||
// 保存到历史记录
|
||||
self.add_to_history(message, &response.content);
|
||||
|
||||
Ok(response.content)
|
||||
}
|
||||
|
||||
/// 发送带单个附件的消息
|
||||
pub async fn send_message_with_attachment(
|
||||
&mut self,
|
||||
message: &str,
|
||||
attachment_path: &str
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
self.send_message_with_attachments(message, vec![attachment_path]).await
|
||||
}
|
||||
|
||||
/// 清除会话历史
|
||||
pub fn clear_conversation(&mut self) {
|
||||
self.conversation_history.clear();
|
||||
}
|
||||
|
||||
/// 获取会话历史
|
||||
pub fn get_conversation_history(&self) -> &Vec<String> {
|
||||
&self.conversation_history
|
||||
}
|
||||
|
||||
/// 构建包含历史记录的消息
|
||||
fn build_message_with_history(&self, message: &str) -> String {
|
||||
if self.conversation_history.is_empty() {
|
||||
message.to_string()
|
||||
} else {
|
||||
// 获取最近的几轮对话作为上下文
|
||||
let recent_history = self.conversation_history
|
||||
.iter()
|
||||
.rev()
|
||||
.take(6) // 最近3轮对话(用户+AI各3次)
|
||||
.rev()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
format!("对话历史:\n{}\n\n当前消息: {}", recent_history, message)
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加到历史记录
|
||||
fn add_to_history(&mut self, user_message: &str, ai_response: &str) {
|
||||
self.conversation_history.push(format!("用户: {}", user_message));
|
||||
self.conversation_history.push(format!("助手: {}", ai_response));
|
||||
|
||||
// 限制历史记录长度,保留最近20条消息
|
||||
if self.conversation_history.len() > 20 {
|
||||
self.conversation_history.drain(0..self.conversation_history.len() - 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user