提交qiming-mcp-proxy

This commit is contained in:
Codex
2026-06-01 13:03:20 +08:00
parent 9e9486b7c2
commit afb3d9f4e6
394 changed files with 124494 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
pub mod model;
pub mod tts;
pub use tts::TtsAction;
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "voice-cli")]
#[command(about = "Speech-to-text HTTP service with CLI interface")]
#[command(version = "0.1.0")]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
/// Configuration file path
#[arg(short, long, default_value = "config.yml")]
pub config: String,
/// Verbose output
#[arg(short, long)]
pub verbose: bool,
}
#[derive(Subcommand)]
pub enum Commands {
/// Server management commands
Server {
#[command(subcommand)]
action: ServerAction,
},
/// Model management commands
Model {
#[command(subcommand)]
action: ModelAction,
},
/// TTS management commands
Tts {
#[command(subcommand)]
action: TtsAction,
},
}
#[derive(Subcommand)]
pub enum ServerAction {
/// Initialize server configuration
Init {
/// Configuration file output path (default: ./server-config.yml)
#[arg(short, long)]
config: Option<std::path::PathBuf>,
/// Force overwrite existing configuration file
#[arg(long)]
force: bool,
},
/// Run server in foreground mode
Run {
/// Configuration file path
#[arg(short, long)]
config: Option<std::path::PathBuf>,
},
}
#[derive(Subcommand)]
pub enum ModelAction {
/// Download a specific model
Download {
/// Model name to download (e.g., base, small, large)
model_name: String,
},
/// List available and downloaded models
List,
/// Validate downloaded models
Validate,
/// Remove a downloaded model
Remove {
/// Model name to remove
model_name: String,
},
/// Diagnose issues with a downloaded model
Diagnose {
/// Model name to diagnose
model_name: String,
},
}
// Daemon mode is no longer supported
// Use foreground mode with shell scripts for background operation

View File

@@ -0,0 +1,242 @@
use crate::VoiceCliError;
use crate::models::Config;
use crate::services::ModelService;
use tracing::{error, info, warn};
pub async fn handle_model_download(config: &Config, model_name: &str) -> crate::Result<()> {
info!("Downloading model: {}", model_name);
// Validate model name
if !config
.whisper
.supported_models
.contains(&model_name.to_string())
{
return Err(VoiceCliError::InvalidModelName(format!(
"Model '{}' is not supported. Supported models: {:?}",
model_name, config.whisper.supported_models
)));
}
let model_service = ModelService::new(config.clone());
// Check if model already exists
if model_service.is_model_downloaded(model_name).await? {
info!("Model '{}' already exists locally", model_name);
return Ok(());
}
// Download the model
match model_service.download_model(model_name).await {
Ok(_) => {
info!("Successfully downloaded model: {}", model_name);
}
Err(e) => {
error!("Failed to download model '{}': {}", model_name, e);
return Err(e);
}
}
Ok(())
}
pub async fn handle_model_list(config: &Config) -> crate::Result<()> {
info!("Listing models...");
let model_service = ModelService::new(config.clone());
println!("\n=== Available Models ===");
for model in &config.whisper.supported_models {
let is_downloaded = model_service
.is_model_downloaded(model)
.await
.unwrap_or(false);
let status = if is_downloaded {
"✓ Downloaded"
} else {
"○ Not Downloaded"
};
let default_marker = if model == &config.whisper.default_model {
" (default)"
} else {
""
};
println!(" {} {}{}", status, model, default_marker);
}
println!("\n=== Downloaded Models Info ===");
let downloaded_models = model_service.list_downloaded_models().await?;
if downloaded_models.is_empty() {
println!(" No models downloaded yet");
} else {
for model_name in downloaded_models {
match model_service.get_model_info(&model_name).await {
Ok(info) => {
println!(
" {} - Size: {}, Status: {}",
model_name, info.size, info.status
);
}
Err(e) => {
println!(" {} - Error: {}", model_name, e);
}
}
}
}
println!("\nModels directory: {}", config.whisper.models_dir);
println!("Default model: {}", config.whisper.default_model);
Ok(())
}
pub async fn handle_model_validate(config: &Config) -> crate::Result<()> {
info!("Validating all downloaded models...");
let model_service = ModelService::new(config.clone());
let downloaded_models = model_service.list_downloaded_models().await?;
if downloaded_models.is_empty() {
info!("No models to validate");
return Ok(());
}
let mut all_valid = true;
for model_name in downloaded_models {
print!("Validating {}... ", model_name);
match model_service.validate_model(&model_name).await {
Ok(_) => {
println!("✓ Valid");
}
Err(e) => {
println!("✗ Invalid: {}", e);
all_valid = false;
}
}
}
if all_valid {
info!("All models are valid");
} else {
warn!("Some models have validation issues");
}
Ok(())
}
pub async fn handle_model_remove(config: &Config, model_name: &str) -> crate::Result<()> {
info!("Removing model: {}", model_name);
let model_service = ModelService::new(config.clone());
// Check if model exists
if !model_service.is_model_downloaded(model_name).await? {
warn!("Model '{}' is not downloaded", model_name);
return Ok(());
}
// Confirm deletion (in a real CLI, you might want user confirmation)
if model_name == config.whisper.default_model {
warn!("Warning: Removing the default model '{}'", model_name);
}
match model_service.remove_model(model_name).await {
Ok(_) => {
info!("Successfully removed model: {}", model_name);
}
Err(e) => {
error!("Failed to remove model '{}': {}", model_name, e);
return Err(e);
}
}
Ok(())
}
/// Interactive model download - downloads the default model if none exist
pub async fn ensure_default_model(config: &Config) -> crate::Result<()> {
let model_service = ModelService::new(config.clone());
// Check if default model exists
if model_service
.is_model_downloaded(&config.whisper.default_model)
.await?
{
return Ok(());
}
// Check if any model exists
let downloaded_models = model_service.list_downloaded_models().await?;
if !downloaded_models.is_empty() {
return Ok(());
}
// No models exist, download default
if config.whisper.auto_download {
info!(
"No models found. Auto-downloading default model: {}",
config.whisper.default_model
);
handle_model_download(config, &config.whisper.default_model).await?;
} else {
return Err(VoiceCliError::ModelNotFound(format!(
"No models found and auto_download is disabled. Please run: voice-cli model download {}",
config.whisper.default_model
)));
}
Ok(())
}
/// Diagnose issues with a downloaded model
pub async fn handle_model_diagnose(config: &Config, model_name: &str) -> crate::Result<()> {
info!("Diagnosing model: {}", model_name);
let model_service = ModelService::new(config.clone());
match model_service.diagnose_model(model_name).await {
Ok(diagnosis) => {
println!("\n=== Model Diagnosis for '{}' ===", model_name);
println!("{}", diagnosis);
// Provide fix suggestions
println!("\n=== Fix Suggestions ===");
if !model_service
.is_model_downloaded(model_name)
.await
.unwrap_or(false)
{
println!(
"💡 Model not found - run: voice-cli model download {}",
model_name
);
} else {
match model_service.validate_model(model_name).await {
Ok(_) => {
println!("✓ Model is valid and ready to use");
}
Err(_) => {
println!("🔧 To fix the corrupted model:");
println!(
" 1. Remove the corrupted file: voice-cli model remove {}",
model_name
);
println!(
" 2. Re-download the model: voice-cli model download {}",
model_name
);
println!(" 3. Validate the model: voice-cli model validate");
}
}
}
}
Err(e) => {
error!("Failed to diagnose model '{}': {}", model_name, e);
return Err(e);
}
}
Ok(())
}

View File

@@ -0,0 +1,122 @@
use clap::Subcommand;
use std::path::PathBuf;
#[derive(Subcommand)]
pub enum TtsAction {
/// Initialize TTS environment
Init {
/// Force overwrite existing environment
#[arg(long)]
force: bool,
},
/// Test TTS functionality
Test {
/// Text to synthesize
#[arg(short, long, default_value = "Hello, world!")]
text: String,
/// Output file path
#[arg(short, long)]
output: Option<PathBuf>,
/// Model to use
#[arg(short, long)]
model: Option<String>,
/// Speech speed (0.5-2.0)
#[arg(short, long, default_value = "1.0")]
speed: f32,
/// Pitch adjustment (-20 to 20)
#[arg(short, long, default_value = "0")]
pitch: i32,
/// Volume adjustment (0.5-2.0)
#[arg(short, long, default_value = "1.0")]
volume: f32,
/// Output format
#[arg(short, long, default_value = "mp3")]
format: String,
},
}
/// Initialize TTS environment
pub async fn handle_tts_init(_force: bool) -> anyhow::Result<()> {
println!("🎤 Initializing TTS environment...");
// Reuse the server init logic for Python environment
crate::server::init_python_tts_environment()
.await
.map_err(|e| anyhow::anyhow!("Failed to initialize TTS environment: {}", e))?;
println!("✅ TTS environment initialized successfully");
Ok(())
}
/// Test TTS functionality
pub async fn handle_tts_test(
config: &crate::Config,
text: String,
output: Option<std::path::PathBuf>,
model: Option<String>,
speed: f32,
pitch: i32,
volume: f32,
format: String,
) -> anyhow::Result<()> {
println!("🎤 Testing TTS functionality...");
// Create TTS service
let tts_service = crate::services::TtsService::new(
config.tts.python_path.clone(),
config.tts.model_path.clone(),
)
.map_err(|e| anyhow::anyhow!("Failed to create TTS service: {}", e))?;
// Create request
let request = crate::models::TtsSyncRequest {
text,
model,
speed: Some(speed),
pitch: Some(pitch),
volume: Some(volume),
format: Some(format),
};
// Test synthesis
let result_path = tts_service
.synthesize_sync(request)
.await
.map_err(|e| anyhow::anyhow!("TTS synthesis failed: {}", e))?;
println!("✅ TTS test successful!");
println!("📁 Output file: {}", result_path.display());
// Copy to specified output path if provided
if let Some(output_path) = output {
tokio::fs::copy(&result_path, &output_path)
.await
.map_err(|e| anyhow::anyhow!("Failed to copy output file: {}", e))?;
println!("📁 Copied to: {}", output_path.display());
}
Ok(())
}
/// Handle TTS-related commands
pub async fn handle_tts_command(action: TtsAction, config: &crate::Config) -> anyhow::Result<()> {
match action {
TtsAction::Init { force } => handle_tts_init(force).await,
TtsAction::Test {
text,
output,
model,
speed,
pitch,
volume,
format,
} => handle_tts_test(config, text, output, model, speed, pitch, volume, format).await,
}
}