CosyVoice命令行工具开发:快速语音合成的Python脚本实战

最近在做一个自动化内容生成的项目,需要把大量文本转成语音。一开始我都是手动调用CosyVoice的API,每次改个参数都要重新写代码,效率实在太低。后来我想,为什么不写个命令行工具呢?就像平时用的gitpip那样,一条命令就能搞定语音合成。

今天我就来分享这个实战经验,教你从零开始写一个轻量级的CosyVoice命令行工具。这个工具能让你:

  • 从文件或直接输入文本生成语音
  • 指定输出格式和保存路径
  • 批量处理多个文本文件
  • 实时显示进度,方便集成到自动化脚本

即使你Python基础一般,跟着做下来也能掌握命令行工具开发的核心技巧。咱们不搞复杂的架构设计,就解决实际问题,让你写出来的工具马上就能用。

1. 环境准备与项目搭建

在开始写代码之前,我们先准备好开发环境。这个工具主要依赖两个库:argparse处理命令行参数,tqdm显示进度条。

1.1 安装必要依赖

打开终端,创建一个新的项目目录,然后安装需要的包:

# 创建项目目录
mkdir cosyvoice-cli
cd cosyvoice-cli

# 创建虚拟环境(推荐)
python -m venv venv

# 激活虚拟环境
# Windows:
venv\Scripts\activate
# Mac/Linux:
source venv/bin/activate

# 安装核心依赖
pip install argparse tqdm

# 安装CosyVoice SDK(根据官方文档选择合适版本)
pip install cosyvoice-sdk

如果你用的是Anaconda,也可以用conda创建环境:

conda create -n cosyvoice-cli python=3.9
conda activate cosyvoice-cli
pip install argparse tqdm cosyvoice-sdk

1.2 项目结构规划

一个好的项目结构能让代码更清晰,也方便后期维护。我建议这样组织文件:

cosyvoice-cli/
├── cosyvoice_cli/
│   ├── __init__.py
│   ├── cli.py          # 命令行入口
│   ├── core.py         # 核心合成逻辑
│   └── utils.py        # 工具函数
├── requirements.txt    # 依赖列表
├── setup.py           # 安装配置
└── README.md          # 使用说明

先创建这些基础文件:

# 创建目录结构
mkdir -p cosyvoice_cli
touch cosyvoice_cli/__init__.py
touch cosyvoice_cli/cli.py
touch cosyvoice_cli/core.py
touch cosyvoice_cli/utils.py
touch requirements.txt
touch setup.py
touch README.md

requirements.txt里写上依赖:

argparse>=1.4.0
tqdm>=4.65.0
cosyvoice-sdk>=1.0.0

这样基础环境就准备好了。接下来我们开始写核心功能。

2. 核心合成功能实现

我们先从最核心的语音合成功能开始。这部分代码会放在core.py里,负责调用CosyVoice的API。

2.1 基础合成函数

打开cosyvoice_cli/core.py,我们先写一个最简单的合成函数:

import os
from pathlib import Path
from typing import Optional, Union

class CosyVoiceSynthesizer:
    """CosyVoice语音合成器"""
    
    def __init__(self, api_key: Optional[str] = None):
        """
        初始化合成器
        
        Args:
            api_key: CosyVoice API密钥,如果为None则尝试从环境变量读取
        """
        self.api_key = api_key or os.getenv("COSYVOICE_API_KEY")
        if not self.api_key:
            raise ValueError("未提供API密钥,请通过参数或环境变量COSYVOICE_API_KEY设置")
        
        # 这里根据CosyVoice SDK的实际API进行调整
        # 假设SDK的初始化方式如下:
        from cosyvoice import CosyVoiceClient
        self.client = CosyVoiceClient(api_key=self.api_key)
    
    def synthesize(
        self,
        text: str,
        output_path: Union[str, Path],
        voice_type: str = "standard",
        speed: float = 1.0,
        format: str = "mp3"
    ) -> bool:
        """
        将文本合成为语音文件
        
        Args:
            text: 要合成的文本
            output_path: 输出文件路径
            voice_type: 音色类型
            speed: 语速(0.5-2.0)
            format: 输出格式(mp3/wav)
            
        Returns:
            bool: 合成是否成功
        """
        try:
            # 确保输出目录存在
            output_dir = os.path.dirname(output_path)
            if output_dir:
                os.makedirs(output_dir, exist_ok=True)
            
            # 调用CosyVoice API
            # 这里根据实际SDK调整调用方式
            audio_data = self.client.synthesize(
                text=text,
                voice=voice_type,
                speed=speed,
                format=format
            )
            
            # 保存音频文件
            with open(output_path, 'wb') as f:
                f.write(audio_data)
            
            print(f"✓ 合成成功: {output_path}")
            return True
            
        except Exception as e:
            print(f"✗ 合成失败: {str(e)}")
            return False

这个类封装了基本的合成功能。我加了详细的参数说明和错误处理,这样用起来更放心。

2.2 批量处理功能

很多时候我们需要一次处理多个文件,比如把一本电子书分成多个章节转成音频。我们来添加批量处理功能:

from tqdm import tqdm
from typing import List

class BatchProcessor:
    """批量处理器"""
    
    def __init__(self, synthesizer: CosyVoiceSynthesizer):
        self.synthesizer = synthesizer
    
    def process_files(
        self,
        input_files: List[str],
        output_dir: str,
        voice_type: str = "standard",
        format: str = "mp3"
    ):
        """
        批量处理多个文本文件
        
        Args:
            input_files: 输入文件列表
            output_dir: 输出目录
            voice_type: 音色类型
            format: 输出格式
        """
        if not os.path.exists(output_dir):
            os.makedirs(output_dir)
        
        success_count = 0
        total_count = len(input_files)
        
        print(f"开始批量处理 {total_count} 个文件...")
        
        # 使用tqdm显示进度条
        for input_file in tqdm(input_files, desc="处理进度"):
            try:
                # 读取文本内容
                with open(input_file, 'r', encoding='utf-8') as f:
                    text = f.read().strip()
                
                if not text:
                    print(f"警告: {input_file} 为空文件,跳过")
                    continue
                
                # 生成输出文件名
                base_name = os.path.splitext(os.path.basename(input_file))[0]
                output_file = os.path.join(output_dir, f"{base_name}.{format}")
                
                # 合成语音
                if self.synthesizer.synthesize(
                    text=text,
                    output_path=output_file,
                    voice_type=voice_type,
                    format=format
                ):
                    success_count += 1
                    
            except Exception as e:
                print(f"处理文件 {input_file} 时出错: {str(e)}")
        
        print(f"\n批量处理完成!成功: {success_count}/{total_count}")

批量处理功能加上了进度条,处理大量文件时能清楚看到进度,不会让人干等着。

3. 命令行接口设计

核心功能写好了,现在来设计命令行接口。我们用argparse库来解析命令行参数。

3.1 基础参数解析

打开cosyvoice_cli/cli.py,先写参数解析部分:

import argparse
import sys
from pathlib import Path
from typing import List

def parse_arguments():
    """解析命令行参数"""
    
    parser = argparse.ArgumentParser(
        description="CosyVoice命令行工具 - 快速语音合成",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
使用示例:
  # 从文本生成语音
  cosyvoice-cli -t "你好,世界" -o hello.mp3
  
  # 从文件生成语音
  cosyvoice-cli -f input.txt -o output.mp3
  
  # 批量处理多个文件
  cosyvoice-cli -b file1.txt file2.txt -d outputs/
  
  # 指定音色和语速
  cosyvoice-cli -t "测试文本" -o test.wav --voice female --speed 1.2
        """
    )
    
    # 输入方式组(互斥)
    input_group = parser.add_mutually_exclusive_group(required=True)
    input_group.add_argument(
        "-t", "--text",
        help="直接输入要合成的文本"
    )
    input_group.add_argument(
        "-f", "--file",
        help="从文件读取文本"
    )
    input_group.add_argument(
        "-b", "--batch",
        nargs="+",
        help="批量处理多个文件"
    )
    
    # 输出参数
    parser.add_argument(
        "-o", "--output",
        required=True,
        help="输出文件路径(单文件)或目录(批量)"
    )
    
    # 音频参数
    parser.add_argument(
        "--voice",
        default="standard",
        choices=["standard", "female", "male", "child"],
        help="音色类型(默认: standard)"
    )
    parser.add_argument(
        "--speed",
        type=float,
        default=1.0,
        help="语速(0.5-2.0,默认: 1.0)"
    )
    parser.add_argument(
        "--format",
        default="mp3",
        choices=["mp3", "wav", "ogg"],
        help="输出格式(默认: mp3)"
    )
    
    # 其他参数
    parser.add_argument(
        "--api-key",
        help="CosyVoice API密钥(也可通过环境变量COSYVOICE_API_KEY设置)"
    )
    parser.add_argument(
        "--verbose",
        action="store_true",
        help="显示详细日志"
    )
    
    return parser.parse_args()

我设计了三种输入方式:直接输入文本、从文件读取、批量处理多个文件。这样能满足不同场景的需求。

3.2 主函数实现

参数解析写好了,现在来实现主函数,把各个部分连接起来:

def main():
    """命令行工具主函数"""
    
    # 解析参数
    args = parse_arguments()
    
    # 初始化合成器
    from .core import CosyVoiceSynthesizer
    try:
        synthesizer = CosyVoiceSynthesizer(api_key=args.api_key)
    except ValueError as e:
        print(f"初始化失败: {e}")
        print("请设置API密钥:")
        print("  1. 通过 --api-key 参数")
        print("  2. 或设置环境变量 COSYVOICE_API_KEY")
        sys.exit(1)
    
    # 根据输入方式处理
    if args.text:
        # 直接输入文本
        handle_text_input(synthesizer, args)
    elif args.file:
        # 从文件读取
        handle_file_input(synthesizer, args)
    elif args.batch:
        # 批量处理
        handle_batch_input(synthesizer, args)
    
    print("任务完成!")

def handle_text_input(synthesizer, args):
    """处理直接文本输入"""
    if args.verbose:
        print(f"合成文本: {args.text[:50]}...")
        print(f"输出文件: {args.output}")
    
    success = synthesizer.synthesize(
        text=args.text,
        output_path=args.output,
        voice_type=args.voice,
        speed=args.speed,
        format=args.format
    )
    
    if not success:
        sys.exit(1)

def handle_file_input(synthesizer, args):
    """处理文件输入"""
    try:
        with open(args.file, 'r', encoding='utf-8') as f:
            text = f.read()
        
        if args.verbose:
            print(f"从文件读取: {args.file}")
            print(f"文本长度: {len(text)} 字符")
        
        success = synthesizer.synthesize(
            text=text,
            output_path=args.output,
            voice_type=args.voice,
            speed=args.speed,
            format=args.format
        )
        
        if not success:
            sys.exit(1)
            
    except FileNotFoundError:
        print(f"错误: 文件不存在 {args.file}")
        sys.exit(1)
    except Exception as e:
        print(f"读取文件失败: {str(e)}")
        sys.exit(1)

def handle_batch_input(synthesizer, args):
    """处理批量输入"""
    from .core import BatchProcessor
    
    processor = BatchProcessor(synthesizer)
    
    # 检查输出路径是否为目录
    output_dir = args.output
    if not os.path.isdir(output_dir):
        print(f"错误: 批量处理时输出路径必须是目录")
        print(f"请创建目录: mkdir -p {output_dir}")
        sys.exit(1)
    
    processor.process_files(
        input_files=args.batch,
        output_dir=output_dir,
        voice_type=args.voice,
        format=args.format
    )

if __name__ == "__main__":
    main()

主函数根据不同的输入方式调用对应的处理函数,逻辑很清晰。错误处理也考虑得比较周全。

4. 打包与安装配置

工具写好了,我们还要让它能像其他命令行工具一样方便安装和使用。

4.1 编写setup.py

创建setup.py文件,这是Python包的安装配置文件:

from setuptools import setup, find_packages

with open("README.md", "r", encoding="utf-8") as fh:
    long_description = fh.read()

setup(
    name="cosyvoice-cli",
    version="0.1.0",
    author="Your Name",
    author_email="your.email@example.com",
    description="命令行工具 for CosyVoice语音合成",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url="https://github.com/yourusername/cosyvoice-cli",
    packages=find_packages(),
    classifiers=[
        "Programming Language :: Python :: 3",
        "Programming Language :: Python :: 3.7",
        "Programming Language :: Python :: 3.8",
        "Programming Language :: Python :: 3.9",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
    python_requires=">=3.7",
    install_requires=[
        "argparse>=1.4.0",
        "tqdm>=4.65.0",
        # CosyVoice SDK根据实际情况添加
    ],
    entry_points={
        "console_scripts": [
            "cosyvoice-cli=cosyvoice_cli.cli:main",
        ],
    },
)

关键在entry_points部分,它告诉Python安装后创建一个叫cosyvoice-cli的命令。

4.2 安装和使用

现在可以安装我们的工具了:

# 开发模式安装(可编辑)
pip install -e .

# 或者普通安装
pip install .

安装后,就可以在命令行使用了:

# 查看帮助
cosyvoice-cli --help

# 测试简单合成
cosyvoice-cli -t "这是一个测试" -o test.mp3

# 从文件合成
cosyvoice-cli -f novel_chapter1.txt -o chapter1.mp3 --voice female

# 批量处理
cosyvoice-cli -b chapter*.txt -d audio_chapters/ --format wav

4.3 编写使用说明

一个好的工具要有清晰的文档。在README.md里写上使用说明:

# CosyVoice命令行工具

一个轻量级的命令行工具,用于快速调用CosyVoice语音合成服务。

## 功能特性

- 📝 支持文本直接输入、文件读取、批量处理
- 🎵 多种音色选择(标准、女声、男声、童声)
- ⚡ 可调节语速(0.5x-2.0x)
- 📁 支持MP3、WAV、OGG格式输出
- 📊 批量处理显示进度条

## 快速开始

1. 安装:
```bash
pip install cosyvoice-cli
  1. 设置API密钥:
export COSYVOICE_API_KEY="your-api-key-here"
  1. 使用示例:
# 简单合成
cosyvoice-cli -t "你好,世界" -o hello.mp3

# 从文件合成
cosyvoice-cli -f input.txt -o output.wav --voice female

# 批量处理
cosyvoice-cli -b file1.txt file2.txt -d outputs/

详细参数

查看完整参数说明:

cosyvoice-cli --help

集成到脚本

可以在Shell脚本中调用:

#!/bin/bash
for chapter in chapters/*.txt; do
    base=$(basename "$chapter" .txt)
    cosyvoice-cli -f "$chapter" -o "audio/${base}.mp3"
done

## 5. 实用技巧与进阶功能

基础功能已经完成了,但要让工具更好用,还需要一些实用技巧。

### 5.1 添加配置文件支持

每次都输API密钥挺麻烦的,我们可以添加配置文件支持:

```python
# 在utils.py中添加
import json
import os
from pathlib import Path

def get_config_dir():
    """获取配置目录"""
    home = Path.home()
    config_dir = home / ".config" / "cosyvoice"
    config_dir.mkdir(parents=True, exist_ok=True)
    return config_dir

def load_config():
    """加载配置文件"""
    config_file = get_config_dir() / "config.json"
    if config_file.exists():
        with open(config_file, 'r') as f:
            return json.load(f)
    return {}

def save_config(config):
    """保存配置文件"""
    config_file = get_config_dir() / "config.json"
    with open(config_file, 'w') as f:
        json.dump(config, f, indent=2)

def set_api_key(api_key):
    """设置API密钥到配置文件"""
    config = load_config()
    config["api_key"] = api_key
    save_config(config)
    print("API密钥已保存到配置文件")

然后在命令行工具里添加配置命令:

# 在cli.py的parse_arguments中添加子命令
subparsers = parser.add_subparsers(dest="command", help="子命令")

# 配置命令
config_parser = subparsers.add_parser("config", help="配置管理")
config_parser.add_argument("--set-api-key", help="设置API密钥")

# 在main函数中处理子命令
if args.command == "config":
    if args.set_api_key:
        from .utils import set_api_key
        set_api_key(args.set_api_key)
    else:
        from .utils import load_config
        config = load_config()
        print("当前配置:")
        for key, value in config.items():
            print(f"  {key}: {value}")
    return

这样用户就可以用cosyvoice-cli config --set-api-key your-key来保存密钥了。

5.2 添加格式转换功能

有时候我们需要在不同格式间转换,可以添加这个功能:

# 在core.py中添加
import subprocess
from pathlib import Path

class AudioConverter:
    """音频格式转换器"""
    
    @staticmethod
    def convert_format(input_file: str, output_file: str, target_format: str):
        """
        转换音频格式(需要安装ffmpeg)
        
        Args:
            input_file: 输入文件
            output_file: 输出文件
            target_format: 目标格式
        """
        try:
            # 检查ffmpeg是否安装
            subprocess.run(["ffmpeg", "-version"], 
                         capture_output=True, check=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            print("错误: 需要安装ffmpeg才能进行格式转换")
            print("安装方法: https://ffmpeg.org/download.html")
            return False
        
        # 构建转换命令
        cmd = [
            "ffmpeg",
            "-i", input_file,
            "-y",  # 覆盖输出文件
            output_file
        ]
        
        try:
            subprocess.run(cmd, capture_output=True, check=True)
            print(f"✓ 格式转换成功: {output_file}")
            return True
        except subprocess.CalledProcessError as e:
            print(f"✗ 格式转换失败: {e.stderr.decode()}")
            return False

5.3 错误处理优化

好的错误提示能让用户更快解决问题。我们优化一下错误处理:

# 在core.py的synthesize方法中添加更详细的错误处理
def synthesize(self, text: str, output_path: str, **kwargs) -> bool:
    """增强版的合成方法"""
    
    # 参数验证
    if not text or not text.strip():
        print("错误: 文本内容不能为空")
        return False
    
    if len(text) > 5000:  # 假设API限制
        print("警告: 文本过长,建议分割成多段")
        # 可以在这里添加自动分割逻辑
    
    # 检查输出路径
    output_path = Path(output_path)
    if output_path.exists():
        print(f"警告: 文件已存在 {output_path}")
        # 可以询问是否覆盖,这里简单跳过
        return False
    
    # 检查目录权限
    output_dir = output_path.parent
    if not os.access(output_dir, os.W_OK):
        print(f"错误: 没有写入权限 {output_dir}")
        return False
    
    # 原来的合成逻辑...

6. 常见问题与解决

在实际使用中,可能会遇到一些问题。这里整理了几个常见问题和解决方法。

问题1:API密钥错误

错误: 认证失败,请检查API密钥

解决方法:

  • 检查密钥是否正确:cosyvoice-cli config查看当前配置
  • 重新设置:cosyvoice-cli config --set-api-key 新密钥
  • 检查环境变量:echo $COSYVOICE_API_KEY

问题2:文本过长

CosyVoice可能有文本长度限制,如果遇到错误可以分割文本:

# 在utils.py中添加文本分割函数
def split_long_text(text, max_length=1000):
    """将长文本分割成多段"""
    paragraphs = text.split('\n')
    chunks = []
    current_chunk = ""
    
    for para in paragraphs:
        if len(current_chunk) + len(para) + 1 <= max_length:
            current_chunk += para + "\n"
        else:
            if current_chunk:
                chunks.append(current_chunk.strip())
            current_chunk = para + "\n"
    
    if current_chunk:
        chunks.append(current_chunk.strip())
    
    return chunks

问题3:网络连接问题

如果API调用超时或失败,可以添加重试机制:

import time
from functools import wraps

def retry_on_failure(max_retries=3, delay=1):
    """失败重试装饰器"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_retries - 1:
                        raise
                    print(f"尝试 {attempt + 1} 失败,{delay}秒后重试...")
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

# 使用装饰器
@retry_on_failure(max_retries=3, delay=2)
def synthesize_with_retry(self, text, output_path):
    # 原来的合成逻辑
    pass

问题4:内存不足

处理大量文件时可能内存不足,可以优化文件处理方式:

def process_large_files(self, file_list, output_dir):
    """处理大文件的优化版本"""
    for input_file in file_list:
        # 逐行读取大文件
        with open(input_file, 'r', encoding='utf-8') as f:
            chunk = []
            chunk_size = 0
            
            for line in f:
                chunk.append(line)
                chunk_size += len(line)
                
                # 达到一定大小就处理
                if chunk_size >= 1000:  # 每1000字符处理一次
                    text = ''.join(chunk)
                    # 合成这一块
                    # ...
                    chunk = []
                    chunk_size = 0
            
            # 处理剩余部分
            if chunk:
                text = ''.join(chunk)
                # 合成
                # ...

7. 总结

写这个CosyVoice命令行工具的过程,其实也是学习Python工程化开发的好机会。从最开始的简单脚本,到后来加入参数解析、错误处理、配置文件、批量处理等功能,工具变得越来越实用。

用下来感觉最大的好处是效率提升。以前要写一堆代码才能完成的工作,现在一行命令就搞定了。批量处理功能特别实用,处理几十个文件也不用一直盯着。

如果你刚开始接触命令行工具开发,建议先从简单的功能开始,然后逐步添加需要的特性。不用追求一次完美,先让工具跑起来,再慢慢优化。遇到问题就查文档、看错误信息,大部分问题都能找到解决方法。

这个工具还有很多可以扩展的地方,比如添加更多音频效果选项、支持流式合成、集成到其他工作流中等等。你可以根据自己的需求来定制。关键是要让工具真正解决实际问题,而不是为了写工具而写工具。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐