feat: Enhance text-to-mic CLI and GUI with model selection and Edge-TTS support

This commit introduces significant improvements to the text-to-mic functionality, including:

- Added command-line argument parsing for better user interaction.
- Support for Edge-TTS, allowing audio generation without an API key.
- Dynamic model and voice selection based on user input and environment variables.
- Improved error handling and user feedback for audio device selection.
- Updated default voice settings based on selected TTS model.
- Removed unused compiled Python files from the repository.

Changes:
- text-to-mic-cli.py: Implemented argument parsing and client initialization logic.
- utils/text_to_mic.py: Integrated Edge-TTS audio generation and updated voice selection logic.
- Cleaned up unnecessary compiled files in the utils/__pycache__ directory.

This update enhances usability and flexibility for users leveraging different TTS models.
This commit is contained in:
Xin Wang
2026-06-18 13:55:03 +08:00
parent 92d20e59e9
commit 3d6ef1833e
8 changed files with 349 additions and 75 deletions

View File

@@ -5,19 +5,58 @@ import wave
import threading
from dotenv import load_dotenv
import os
import argparse
# Load environment variables from .env file
load_dotenv()
# Set up your OpenAI API key from the environment variable
api_key = os.getenv('OPENAI_API_KEY')
# Default API config (will be overridden by CLI args if provided)
api_key = os.getenv('OPENAI_API_KEY', '')
api_base_url = os.getenv('OPENAI_API_BASE_URL', '').strip()
# Create client with custom base URL if provided
if api_base_url:
client = OpenAI(api_key=api_key, base_url=api_base_url)
else:
client = OpenAI(api_key=api_key)
# Client will be created after args are parsed
client = None
def get_client(key=None, base_url=None):
"""Get or create the OpenAI client."""
global client
effective_key = key or api_key
effective_base = (base_url or api_base_url).strip()
if not effective_key:
raise ValueError("API key is required. Set OPENAI_API_KEY env var or use --api-key")
if effective_base:
return OpenAI(api_key=effective_key, base_url=effective_base)
return OpenAI(api_key=effective_key)
# Model name aliases
MODEL_ALIASES = {
'cosyvoice2': 'FunAudioLLM/CosyVoice2-0.5B',
'cosyvoice': 'FunAudioLLM/CosyVoice2-0.5B',
'tts-1': 'tts-1',
'tts-1-hd': 'tts-1-hd',
'gpt-4o-mini-tts': 'gpt-4o-mini-tts',
}
# Map alias to full model name
def resolve_model(model_input):
"""Resolve model alias to full model name."""
if not model_input:
return None
return MODEL_ALIASES.get(model_input.lower(), model_input)
# Check if using SiliconFlow
def is_siliconflow():
"""Check if the API is configured for SiliconFlow."""
return api_base_url and 'siliconflow' in api_base_url.lower()
# Format voice for CosyVoice2
def format_cosyvoice2_voice(voice, model):
"""Format voice for CosyVoice2: model:voice"""
if model and 'CosyVoice2' in model and is_siliconflow():
return f"{model}:{voice}"
return voice
def list_audio_devices():
p = pyaudio.PyAudio()
@@ -88,14 +127,21 @@ def play_audio_multiplexed(file_paths, device_indices):
p.terminate()
def stream_audio_to_virtual_mic(text, voice="fable", model=None, device_index=None, device_index_2=None):
def stream_audio_to_virtual_mic(text, voice="fable", model=None, device_index=None, device_index_2=None, api_key=None, api_base=None):
# Get model from environment variable or use default
if model is None:
model = os.getenv('OPENAI_TTS_MODEL', 'tts-1')
response = client.audio.speech.create(
# Format voice for CosyVoice2 if using SiliconFlow
voice_to_use = format_cosyvoice2_voice(voice, model)
print(f"Using model: {model}, voice: {voice_to_use}")
# Get client with potential CLI overrides
effective_client = get_client(api_key, api_base)
response = effective_client.audio.speech.create(
model=model,
voice=voice,
voice=voice_to_use,
input=text,
response_format='wav'
)
@@ -118,44 +164,100 @@ def stream_audio_to_virtual_mic(text, voice="fable", model=None, device_index=No
if __name__ == "__main__":
import sys
parser = argparse.ArgumentParser(
description='Text-to-Mic CLI: Convert text to speech and play to virtual microphone',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
%(prog)s "Hello world"
%(prog)s "Hello world" --voice anna --model cosyvoice2
%(prog)s "Hello world" --voice alex --model FunAudioLLM/CosyVoice2-0.5B --device 8
%(prog)s --list-devices
%(prog)s --list-voices
Environment variables (optional, CLI args take precedence):
OPENAI_API_KEY - Your API key (required for API TTS)
OPENAI_API_BASE_URL - Custom API base URL (e.g., https://api.siliconflow.cn/v1)
OPENAI_TTS_MODEL - Default TTS model (default: tts-1)
arglen = len(sys.argv)
Model aliases:
cosyvoice2, cosyvoice -> FunAudioLLM/CosyVoice2-0.5B
tts-1 -> tts-1
tts-1-hd -> tts-1-hd
gpt-4o-mini-tts -> gpt-4o-mini-tts
if arglen < 2:
print("Usage: python script.py 'text to convert'")
print("Environment variables:")
print(" OPENAI_API_KEY - Your API key (required)")
print(" OPENAI_API_BASE_URL - Custom API base URL (optional)")
print(" OPENAI_TTS_MODEL - TTS model to use (default: tts-1)")
print("")
print("Example models:")
print(" - tts-1 (OpenAI standard)")
print(" - tts-1-hd (OpenAI high quality)")
print(" - gpt-4o-mini-tts (OpenAI)")
print(" - FunAudioLLM/CosyVoice2-0.5B (SiliconFlow)")
print("")
print("For SiliconFlow voices with CosyVoice2:")
print(" The voice will be auto-formatted as: FunAudioLLM/CosyVoice2-0.5B:alex")
sys.exit(1)
SiliconFlow CosyVoice2 voices:
alex, anna, bella, benjamin, charles, claire, david, diana
print(f"arg count {arglen}")
OpenAI voices:
alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer
'''
)
# Get TTS model from environment
tts_model = os.getenv('OPENAI_TTS_MODEL', 'tts-1')
print(f"Using TTS model: {tts_model}")
parser.add_argument('text', nargs='?', help='Text to convert to speech')
parser.add_argument('--voice', '-v', default='fable',
help='Voice to use (default: fable). For CosyVoice2: alex, anna, bella, etc.')
parser.add_argument('--model', '-m', default=None,
help='TTS model to use. Use alias (cosyvoice2) or full name (FunAudioLLM/CosyVoice2-0.5B)')
parser.add_argument('--device', '-d', type=int, default=None,
help='Audio device index to play to (use --list-devices to find)')
parser.add_argument('--device2', type=int, default=None,
help='Second audio device index for multiplexed playback')
parser.add_argument('--list-devices', action='store_true',
help='List available audio output devices and exit')
parser.add_argument('--list-voices', action='store_true',
help='List available voices for the configured model and exit')
parser.add_argument('--list-models', action='store_true',
help='List available model aliases and exit')
parser.add_argument('--api-key', default=None,
help='OpenAI API key (or set OPENAI_API_KEY env var)')
parser.add_argument('--api-base', default=None,
help='API base URL (or set OPENAI_API_BASE_URL env var)')
if arglen == 4:
device_index = int(sys.argv[2])
device_index_2 = int(sys.argv[3])
elif arglen == 3:
device_index = int(sys.argv[2])
device_index_2 = None
else:
args = parser.parse_args()
# Handle list commands
if args.list_devices:
list_audio_devices()
exit(0)
if args.list_models:
print("Available model aliases:")
for alias, full_name in MODEL_ALIASES.items():
print(f" {alias:20} -> {full_name}")
exit(0)
if args.list_voices:
# Detect if using SiliconFlow based on API base URL
if is_siliconflow():
print("SiliconFlow CosyVoice2 voices:")
voices = ['alex', 'anna', 'bella', 'benjamin', 'charles', 'claire', 'david', 'diana']
else:
print("OpenAI voices:")
voices = ['alloy', 'ash', 'ballad', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer']
for voice in voices:
print(f" {voice}")
exit(0)
# Validate text argument
if not args.text:
parser.print_help()
exit(1)
# Resolve model (alias -> full name)
model = resolve_model(args.model) if args.model else os.getenv('OPENAI_TTS_MODEL', 'tts-1')
print(f"Text: {args.text}")
print(f"Voice: {args.voice}")
print(f"Model: {model}")
# Get device index
device_index = args.device
device_index_2 = args.device2
if device_index is None:
list_audio_devices()
device_index = int(input("Enter the device index: "))
device_index_2 = None
stream_audio_to_virtual_mic(sys.argv[1], voice="fable", model=tts_model, device_index=device_index,device_index_2=device_index_2)
stream_audio_to_virtual_mic(args.text, voice=args.voice, model=model,
device_index=device_index, device_index_2=device_index_2,
api_key=args.api_key, api_base=args.api_base)