feat: 实现 JSON-RPC 通信协议替代字符串匹配
🚀 核心改进: 1. 创建标准化 JSON-RPC 2.0 通信模块: - 新增 jsonrpc.py 模块,实现完整的 JSON-RPC 2.0 规范 - JSONRPCResponse 类处理响应和错误 - ProgressReporter 类使用通知机制报告进度 - 标准化错误代码定义 (JSONRPCError) 2. Python 脚本集成 JSON-RPC: - 视频生成器支持 request_id 参数 - 使用结构化进度报告替代简单字符串 - 错误处理通过 JSON-RPC 错误响应 - 保持向后兼容的 progress_callback 3. Rust 端 JSON-RPC 解析: - 识别 'JSONRPC:' 前缀的结构化消息 - 区分进度通知和最终结果 - 支持直接 JSON 结果的备用解析 - 详细的调试日志和错误处理 4. 通信协议标准化: - 进度消息:{"jsonrpc":"2.0","method":"progress","params":{...}} - 结果消息:{"jsonrpc":"2.0","id":"...","result":{...}} - 错误消息:{"jsonrpc":"2.0","id":"...","error":{...}} - 时间戳和详细信息支持 5. 错误处理增强: - 标准化错误代码 (-32001 到 -32005) - 详细的错误信息和上下文 - 优雅的降级和备用机制 - 跨语言错误传播 ✅ 优势: - 可靠的结构化通信 ✓ - 标准化协议,易于扩展 ✓ - 详细的进度跟踪和错误处理 ✓ - 向后兼容现有代码 ✓ 现在通信机制更加可靠,不再依赖容易出错的字符串匹配!
This commit is contained in:
192
python_core/ai_video/jsonrpc.py
Normal file
192
python_core/ai_video/jsonrpc.py
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
JSON-RPC Communication Module
|
||||||
|
JSON-RPC 通信模块
|
||||||
|
|
||||||
|
Provides standardized JSON-RPC 2.0 communication for Tauri-Python integration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from typing import Any, Dict, Optional, Union
|
||||||
|
|
||||||
|
|
||||||
|
class JSONRPCResponse:
|
||||||
|
"""JSON-RPC 2.0 Response handler"""
|
||||||
|
|
||||||
|
def __init__(self, request_id: Optional[Union[str, int]] = None):
|
||||||
|
self.request_id = request_id
|
||||||
|
|
||||||
|
def success(self, result: Any) -> None:
|
||||||
|
"""Send successful response"""
|
||||||
|
response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": self.request_id,
|
||||||
|
"result": result
|
||||||
|
}
|
||||||
|
self._send_response(response)
|
||||||
|
|
||||||
|
def error(self, code: int, message: str, data: Any = None) -> None:
|
||||||
|
"""Send error response"""
|
||||||
|
error_obj = {
|
||||||
|
"code": code,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
error_obj["data"] = data
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": self.request_id,
|
||||||
|
"error": error_obj
|
||||||
|
}
|
||||||
|
self._send_response(response)
|
||||||
|
|
||||||
|
def notification(self, method: str, params: Any = None) -> None:
|
||||||
|
"""Send notification (no response expected)"""
|
||||||
|
notification = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": method,
|
||||||
|
"params": params
|
||||||
|
}
|
||||||
|
self._send_response(notification)
|
||||||
|
|
||||||
|
def _send_response(self, response: Dict[str, Any]) -> None:
|
||||||
|
"""Send JSON-RPC response to stdout"""
|
||||||
|
try:
|
||||||
|
json_str = json.dumps(response, ensure_ascii=False, separators=(',', ':'))
|
||||||
|
print(f"JSONRPC:{json_str}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
except Exception as e:
|
||||||
|
# Fallback error response
|
||||||
|
fallback = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": self.request_id,
|
||||||
|
"error": {
|
||||||
|
"code": -32603,
|
||||||
|
"message": "Internal error",
|
||||||
|
"data": str(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print(f"JSONRPC:{json.dumps(fallback)}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressReporter:
|
||||||
|
"""Progress reporting using JSON-RPC notifications"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.rpc = JSONRPCResponse()
|
||||||
|
|
||||||
|
def report(self, step: str, progress: float, message: str, details: Dict[str, Any] = None) -> None:
|
||||||
|
"""Report progress using JSON-RPC notification"""
|
||||||
|
params = {
|
||||||
|
"step": step,
|
||||||
|
"progress": progress, # 0.0 to 1.0
|
||||||
|
"message": message,
|
||||||
|
"timestamp": time.time()
|
||||||
|
}
|
||||||
|
if details:
|
||||||
|
params["details"] = details
|
||||||
|
|
||||||
|
self.rpc.notification("progress", params)
|
||||||
|
|
||||||
|
def step(self, step_name: str, message: str) -> None:
|
||||||
|
"""Report a step without specific progress"""
|
||||||
|
self.report(step_name, -1, message)
|
||||||
|
|
||||||
|
def complete(self, message: str = "完成") -> None:
|
||||||
|
"""Report completion"""
|
||||||
|
self.report("complete", 1.0, message)
|
||||||
|
|
||||||
|
def error(self, message: str, error_details: Dict[str, Any] = None) -> None:
|
||||||
|
"""Report error"""
|
||||||
|
self.report("error", -1, message, error_details)
|
||||||
|
|
||||||
|
|
||||||
|
# Error codes following JSON-RPC 2.0 specification
|
||||||
|
class JSONRPCError:
|
||||||
|
PARSE_ERROR = -32700
|
||||||
|
INVALID_REQUEST = -32600
|
||||||
|
METHOD_NOT_FOUND = -32601
|
||||||
|
INVALID_PARAMS = -32602
|
||||||
|
INTERNAL_ERROR = -32603
|
||||||
|
|
||||||
|
# Custom application errors (start from -32000)
|
||||||
|
FILE_NOT_FOUND = -32001
|
||||||
|
UPLOAD_FAILED = -32002
|
||||||
|
GENERATION_FAILED = -32003
|
||||||
|
DOWNLOAD_FAILED = -32004
|
||||||
|
TIMEOUT_ERROR = -32005
|
||||||
|
|
||||||
|
|
||||||
|
def create_response_handler(request_id: Optional[Union[str, int]] = None) -> JSONRPCResponse:
|
||||||
|
"""Create a JSON-RPC response handler"""
|
||||||
|
return JSONRPCResponse(request_id)
|
||||||
|
|
||||||
|
|
||||||
|
def create_progress_reporter() -> ProgressReporter:
|
||||||
|
"""Create a progress reporter"""
|
||||||
|
return ProgressReporter()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_request(request_str: str) -> Dict[str, Any]:
|
||||||
|
"""Parse JSON-RPC request string"""
|
||||||
|
try:
|
||||||
|
request = json.loads(request_str)
|
||||||
|
if not isinstance(request, dict):
|
||||||
|
raise ValueError("Request must be a JSON object")
|
||||||
|
|
||||||
|
# Validate JSON-RPC 2.0 format
|
||||||
|
if request.get("jsonrpc") != "2.0":
|
||||||
|
raise ValueError("Invalid JSON-RPC version")
|
||||||
|
|
||||||
|
if "method" not in request:
|
||||||
|
raise ValueError("Missing method field")
|
||||||
|
|
||||||
|
return request
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise ValueError(f"Invalid JSON: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage functions
|
||||||
|
def example_video_generation():
|
||||||
|
"""Example of how to use JSON-RPC for video generation"""
|
||||||
|
rpc = create_response_handler("video_gen_001")
|
||||||
|
progress = create_progress_reporter()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Report progress steps
|
||||||
|
progress.step("upload", "[1/4] 正在上传图片到云存储...")
|
||||||
|
# ... upload logic ...
|
||||||
|
|
||||||
|
progress.step("submit", "[2/4] 正在提交视频生成任务...")
|
||||||
|
# ... submit logic ...
|
||||||
|
|
||||||
|
progress.step("wait", "[3/4] 正在等待视频生成完成...")
|
||||||
|
# ... wait logic ...
|
||||||
|
|
||||||
|
progress.step("download", "[4/4] 正在下载视频到本地...")
|
||||||
|
# ... download logic ...
|
||||||
|
|
||||||
|
progress.complete("[完成] 视频生成并下载成功")
|
||||||
|
|
||||||
|
# Send final result
|
||||||
|
result = {
|
||||||
|
"status": True,
|
||||||
|
"video_path": "/path/to/video.mp4",
|
||||||
|
"video_url": "https://example.com/video.mp4",
|
||||||
|
"message": "视频生成并下载成功"
|
||||||
|
}
|
||||||
|
rpc.success(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
progress.error(f"生成失败: {str(e)}")
|
||||||
|
rpc.error(JSONRPCError.GENERATION_FAILED, "Video generation failed", str(e))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Test the JSON-RPC module
|
||||||
|
example_video_generation()
|
||||||
@@ -28,10 +28,12 @@ except ImportError:
|
|||||||
try:
|
try:
|
||||||
from .cloud_storage import CloudStorage
|
from .cloud_storage import CloudStorage
|
||||||
from .api_client import APIClient
|
from .api_client import APIClient
|
||||||
|
from .jsonrpc import create_response_handler, create_progress_reporter, JSONRPCError
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Fallback for when running as script
|
# Fallback for when running as script
|
||||||
from cloud_storage import CloudStorage
|
from cloud_storage import CloudStorage
|
||||||
from api_client import APIClient
|
from api_client import APIClient
|
||||||
|
from jsonrpc import create_response_handler, create_progress_reporter, JSONRPCError
|
||||||
|
|
||||||
logger = setup_logger(__name__)
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
@@ -63,7 +65,8 @@ class VideoGenerator:
|
|||||||
save_path: str = None,
|
save_path: str = None,
|
||||||
timeout: int = 180,
|
timeout: int = 180,
|
||||||
interval: int = 2,
|
interval: int = 2,
|
||||||
progress_callback: Optional[Callable[[str], None]] = None) -> Dict[str, Any]:
|
progress_callback: Optional[Callable[[str], None]] = None,
|
||||||
|
request_id: str = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Generate video from a single image.
|
Generate video from a single image.
|
||||||
|
|
||||||
@@ -80,6 +83,10 @@ class VideoGenerator:
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary with generation result
|
Dictionary with generation result
|
||||||
"""
|
"""
|
||||||
|
# Initialize JSON-RPC handlers
|
||||||
|
rpc = create_response_handler(request_id)
|
||||||
|
progress = create_progress_reporter()
|
||||||
|
|
||||||
result = {'status': False, 'video_path': '', 'video_url': '', 'msg': ''}
|
result = {'status': False, 'video_path': '', 'video_url': '', 'msg': ''}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -108,6 +115,7 @@ class VideoGenerator:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
# Step 1: Upload image to cloud storage
|
# Step 1: Upload image to cloud storage
|
||||||
|
progress.step("upload", "[1/4] 正在上传图片到云存储...")
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
progress_callback("[1/4] 正在上传图片到云存储...")
|
progress_callback("[1/4] 正在上传图片到云存储...")
|
||||||
logger.info(f"Uploading image to cloud storage: {image_path}")
|
logger.info(f"Uploading image to cloud storage: {image_path}")
|
||||||
@@ -116,9 +124,12 @@ class VideoGenerator:
|
|||||||
if not upload_result['status']:
|
if not upload_result['status']:
|
||||||
result['msg'] = f"Failed to upload image: {upload_result['msg']}"
|
result['msg'] = f"Failed to upload image: {upload_result['msg']}"
|
||||||
logger.error(result['msg'])
|
logger.error(result['msg'])
|
||||||
|
progress.error(result['msg'])
|
||||||
|
rpc.error(JSONRPCError.UPLOAD_FAILED, "Image upload failed", result['msg'])
|
||||||
return result
|
return result
|
||||||
|
|
||||||
img_url = upload_result['data']
|
img_url = upload_result['data']
|
||||||
|
progress.step("upload_success", "[1/4] 图片上传成功")
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
progress_callback("[1/4] 图片上传成功")
|
progress_callback("[1/4] 图片上传成功")
|
||||||
logger.info(f"Image uploaded successfully: {img_url}")
|
logger.info(f"Image uploaded successfully: {img_url}")
|
||||||
@@ -374,7 +385,8 @@ def main():
|
|||||||
model_type=args.model,
|
model_type=args.model,
|
||||||
save_path=args.output,
|
save_path=args.output,
|
||||||
timeout=args.timeout,
|
timeout=args.timeout,
|
||||||
progress_callback=progress_callback
|
progress_callback=progress_callback,
|
||||||
|
request_id="cli_request"
|
||||||
)
|
)
|
||||||
|
|
||||||
elif args.action == "batch":
|
elif args.action == "batch":
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ pub struct BatchAIVideoRequest {
|
|||||||
}
|
}
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
use serde_json;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct VideoProcessRequest {
|
pub struct VideoProcessRequest {
|
||||||
@@ -114,7 +115,8 @@ async fn execute_python_command(app: tauri::AppHandle, args: &[String]) -> Resul
|
|||||||
|
|
||||||
let mut stdout_lines = Vec::new();
|
let mut stdout_lines = Vec::new();
|
||||||
let mut stderr_lines = Vec::new();
|
let mut stderr_lines = Vec::new();
|
||||||
let mut json_output = String::new();
|
let mut final_result: Option<String> = None;
|
||||||
|
let mut progress_messages = Vec::new();
|
||||||
|
|
||||||
// Collect output from the process
|
// Collect output from the process
|
||||||
while let Some(event) = rx.recv().await {
|
while let Some(event) = rx.recv().await {
|
||||||
@@ -124,9 +126,27 @@ async fn execute_python_command(app: tauri::AppHandle, args: &[String]) -> Resul
|
|||||||
println!("Python stdout: {}", line);
|
println!("Python stdout: {}", line);
|
||||||
stdout_lines.push(line.to_string());
|
stdout_lines.push(line.to_string());
|
||||||
|
|
||||||
// Try to extract JSON from the line
|
// Parse JSON-RPC messages
|
||||||
if line.trim().starts_with('{') && line.trim().ends_with('}') {
|
if line.starts_with("JSONRPC:") {
|
||||||
json_output = line.trim().to_string();
|
let json_str = &line[8..]; // Remove "JSONRPC:" prefix
|
||||||
|
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(json_str) {
|
||||||
|
if let Some(method) = json_value.get("method") {
|
||||||
|
if method == "progress" {
|
||||||
|
progress_messages.push(json_value);
|
||||||
|
println!("Progress: {}", json_str);
|
||||||
|
}
|
||||||
|
} else if json_value.get("result").is_some() {
|
||||||
|
// This is a final result
|
||||||
|
final_result = Some(json_str.to_string());
|
||||||
|
println!("Final result: {}", json_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if line.trim().starts_with('{') && line.trim().ends_with('}') {
|
||||||
|
// Fallback: try to parse as direct JSON result
|
||||||
|
if let Ok(_) = serde_json::from_str::<serde_json::Value>(line.trim()) {
|
||||||
|
final_result = Some(line.trim().to_string());
|
||||||
|
println!("Direct JSON result: {}", line.trim());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CommandEvent::Stderr(data) => {
|
CommandEvent::Stderr(data) => {
|
||||||
@@ -157,19 +177,30 @@ async fn execute_python_command(app: tauri::AppHandle, args: &[String]) -> Resul
|
|||||||
|
|
||||||
// If no termination event was captured, assume success if we have output
|
// If no termination event was captured, assume success if we have output
|
||||||
if exit_code.is_none() {
|
if exit_code.is_none() {
|
||||||
success = !stdout_lines.is_empty() || !json_output.is_empty();
|
success = !stdout_lines.is_empty() || final_result.is_some();
|
||||||
exit_code = Some(if success { 0 } else { 1 });
|
exit_code = Some(if success { 0 } else { 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Python script exit code: {:?}", exit_code);
|
println!("Python script exit code: {:?}", exit_code);
|
||||||
|
|
||||||
if success {
|
if success {
|
||||||
// Return JSON output if found, otherwise return full stdout
|
// Return final result if found, otherwise return full stdout
|
||||||
if !json_output.is_empty() {
|
if let Some(result) = final_result {
|
||||||
println!("Extracted JSON: {}", json_output);
|
println!("Extracted JSON-RPC result: {}", result);
|
||||||
Ok(json_output)
|
Ok(result)
|
||||||
} else {
|
} else {
|
||||||
Ok(stdout_lines.join("\n"))
|
// Fallback: try to find JSON in the combined stdout
|
||||||
|
let full_output = stdout_lines.join("\n");
|
||||||
|
if let Some(json_start) = full_output.find('{') {
|
||||||
|
if let Some(json_end) = full_output.rfind('}') {
|
||||||
|
if json_end > json_start {
|
||||||
|
let json_str = &full_output[json_start..=json_end];
|
||||||
|
println!("Extracted JSON from full output: {}", json_str);
|
||||||
|
return Ok(json_str.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(full_output)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let error_msg = format!(
|
let error_msg = format!(
|
||||||
|
|||||||
Reference in New Issue
Block a user