提交qiming-mcp-proxy
This commit is contained in:
88
qiming-mcp-proxy/voice-cli/src/cli/mod.rs
Normal file
88
qiming-mcp-proxy/voice-cli/src/cli/mod.rs
Normal 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
|
||||
242
qiming-mcp-proxy/voice-cli/src/cli/model.rs
Normal file
242
qiming-mcp-proxy/voice-cli/src/cli/model.rs
Normal 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(())
|
||||
}
|
||||
122
qiming-mcp-proxy/voice-cli/src/cli/tts.rs
Normal file
122
qiming-mcp-proxy/voice-cli/src/cli/tts.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user