diff --git a/.gitignore b/.gitignore index 26e8a09..7f3a7af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .env -*.wav \ No newline at end of file +*.wav +*.mp3 \ No newline at end of file diff --git a/Readme.md b/Readme.md index c4a25b0..5501505 100644 --- a/Readme.md +++ b/Readme.md @@ -1,21 +1,32 @@ #Readme +This script uses OpenAI to convert text to speech, and then speak that speech over a virtual microphone + +Install VB-Cable: +https://vb-audio.com/Cable/ + +This sets up a virtual microphone that we can use to sent text to speech audio to. Then, when you join a meeting, such as a google meeting, you can select this virtual cable to hear the audio being sent on the channel. +#Dependencies -##Windows +To get this script working you will need to install the following on the relevant operating system -pip install numpy -pip install sounddevice +###Windows + +pip install pyaudio pip install python-dotenv +pip install wave +pip install pydub -##Mac +###Mac -pip install numpy -pip install sounddevice +brew install portaudio pip install python-dotenv +pip install wave +pip install pydub diff --git a/text-to-mic.py b/text-to-mic.py index 7e3842d..b1e6566 100644 --- a/text-to-mic.py +++ b/text-to-mic.py @@ -1,8 +1,8 @@ import openai from openai import OpenAI -import numpy as np -import sounddevice as sd -import soundfile as sf +import pyaudio +import wave +import threading from dotenv import load_dotenv import os @@ -13,50 +13,120 @@ load_dotenv() client = OpenAI(api_key=os.getenv('OPENAI_API_KEY')) def list_audio_devices(): + p = pyaudio.PyAudio() print("Available audio devices:") - devices = sd.query_devices() - for index, device in enumerate(devices): - if device['max_output_channels'] > 0: - print(f"Device index {index}: {device['name']}") + info = p.get_host_api_info_by_index(0) + num_devices = info.get('deviceCount') + # List all available devices, and mark output devices + for i in range(0, num_devices): + if p.get_device_info_by_index(i).get('maxOutputChannels') > 0: + print(f"Device index {i}: {p.get_device_info_by_index(i).get('name')}") + p.terminate() -def stream_audio_to_virtual_mic(text, voice="fable", device_index=None): - # Check if device_index is provided, if not, prompt for it - if device_index is None: - device_index = int(input("Enter the device index: ")) +def play_saved_audio(file_path, device_index=None): + # Open the saved audio file + wf = wave.open(file_path, 'rb') - # Create audio stream from text input + print(f"Playing audio to device {device_index}") + # Setup PyAudio + p = pyaudio.PyAudio() + + try: + stream = p.open(format=p.get_format_from_width(wf.getsampwidth()), + channels=wf.getnchannels(), + rate=wf.getframerate(), + output=True, + output_device_index=device_index) + data = wf.readframes(1024) + while data: + stream.write(data) + data = wf.readframes(1024) + except Exception as e: + print(f"Error playing audio on device {device_index}: {e}") + finally: + stream.stop_stream() + stream.close() + wf.close() + p.terminate() + +#Plays to multiple device indexes at the same time +def play_audio_multiplexed(file_paths, device_indices): + p = pyaudio.PyAudio() + streams = [] + + # Open all files and start all streams + for file_path, device_index in zip(file_paths, device_indices): + wf = wave.open(file_path, 'rb') + stream = p.open(format=p.get_format_from_width(wf.getsampwidth()), + channels=wf.getnchannels(), + rate=wf.getframerate(), + output=True, + output_device_index=device_index) + streams.append((stream, wf)) + + # Play interleaved + active_streams = len(streams) + while active_streams > 0: + for stream, wf in streams: + data = wf.readframes(1024) + if data: + stream.write(data) + else: + stream.stop_stream() + stream.close() + wf.close() + active_streams -= 1 + + p.terminate() + +def stream_audio_to_virtual_mic(text, voice="fable", device_index=None, device_index_2=None): response = client.audio.speech.create( model="tts-1", voice=voice, - input=text + input=text, + response_format='wav' ) + #This can either stream to one device index at a time, or, via multiplexing + #it can stream to two similtaneously to prevent lag playing in sequence + if device_index_2 is not None: + file_path_1 = "output1.wav" + file_path_2 = "output2.wav" + response.stream_to_file(file_path_1) + response.stream_to_file(file_path_2) + play_audio_multiplexed([file_path_1, file_path_2], [device_index, device_index_2]) + else: + file_path_1 = "output1.wav" + response.stream_to_file(file_path_1) + play_saved_audio(file_path_1, device_index) - if device_index is None: - device_index = int(input("Enter the device index: ")) - - # Load the binary audio content into a numpy array - audio_data = np.frombuffer(response.content, dtype=np.int16) - - # Set the samplerate assumed from OpenAI's API (check documentation for exact rate) - sample_rate = 22050 # or 44100, depending on the API's output format - - # Play audio - sd.play(audio_data, sample_rate, device=device_index) - sd.wait() # Wait until the audio has finished playing - - sf.write('captured_audio.wav', audio_data, sample_rate) - + return ""; + if __name__ == "__main__": import sys - if len(sys.argv) < 2: + + arglen = len(sys.argv) + + if arglen < 2: print("Usage: python script.py 'text to convert'") sys.exit(1) - list_audio_devices() - device_index = int(input("Enter the device index: ")) - stream_audio_to_virtual_mic(sys.argv[1], voice="fable", device_index=device_index) + print(f"arg count {arglen}") + + 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: + 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", device_index=device_index,device_index_2=device_index_2)