├── gpt_bot ├── __init__.py ├── speak.py ├── record.py └── __main__.py ├── .gitignore ├── requirements.txt ├── setup.py └── README.md /gpt_bot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | env 3 | prompts -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | openai 3 | prompt_toolkit 4 | pyaudio 5 | tiktoken 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "rt", encoding="utf-8") as file: 4 | long_description = file.read() 5 | 6 | with open('requirements.txt') as f: 7 | install_requires = f.read().splitlines() 8 | 9 | setup( 10 | name="gpt_bot", 11 | version="0.0.3", 12 | description="GPT Commandline interface bot", 13 | author="Hao Zhang", 14 | author_email="zh970205@mail.ustc.edu.cn", 15 | url="https://github.com/hzhangxyz/gpt-bot", 16 | packages=find_packages(), 17 | license="GPLv3", 18 | long_description=long_description, 19 | long_description_content_type="text/markdown", 20 | install_requires=install_requires, 21 | python_requires=">=3.9", 22 | ) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gpt-bot 2 | 3 | A GPT Command line interface bot. 4 | 5 | This is only for gpt-3.5-turbo currently. 6 | You need to set `OPENAI_API_KEY` in environment variable. 7 | And maybe `OPENAI_PROXY` is also useful if you need proxy. 8 | 9 | ## Usage 10 | 11 | `python -m gpt_bot` and enjoy. 12 | 13 | ## Command 14 | 15 | - `/multiline`: toggle multiline input. 16 | - `/prompt`: change [system content](https://platform.openai.com/docs/guides/chat) during chatting. 17 | - `/rollback`: rollback the last conversation. 18 | - `/history`: show chat history. 19 | - `/edit`: edit what bot said just now. 20 | - `/record`: use openai's [whister](https://platform.openai.com/docs/guides/speech-to-text) to transcribe what you are saying. 21 | - `/quit`: quit. 22 | -------------------------------------------------------------------------------- /gpt_bot/speak.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import asyncio 3 | import pyaudio 4 | import tempfile 5 | import wave 6 | 7 | CHUNK = 1024 8 | 9 | 10 | def url_and_param_tts(): 11 | url = "https://tts.baidu.com/text2audio" 12 | params = { 13 | "cuid": "me", 14 | "ctp": 1, 15 | "lan": "zh", 16 | "aue": 6, 17 | "pdt": 301, 18 | } 19 | return url, params 20 | 21 | 22 | def url_and_param_tsn(): 23 | url = "https://tsn.baidu.com/text2audio" 24 | params = { 25 | "tok": ACCESS_TOKEN, 26 | "cuid": "me", 27 | "ctp": 1, 28 | "lan": "zh", 29 | "aue": 6, 30 | "per": 3, 31 | } 32 | return url, params 33 | 34 | 35 | class StreamContext: 36 | 37 | def __init__(self, device, wave_file, *args, **kwargs): 38 | self.device = device 39 | self.wave_file = wave_file 40 | self.args = args 41 | self.kwargs = kwargs 42 | 43 | def __enter__(self): 44 | self.stream = self.device.open(*self.args, **self.kwargs) 45 | return self 46 | 47 | def __exit__(self, *args): 48 | self.stream.close() 49 | 50 | def keep_play(self): 51 | while True: 52 | data = self.wave_file.readframes(CHUNK) 53 | self.stream.write(data) 54 | if len(data) == 0: 55 | break 56 | 57 | 58 | class Player: 59 | 60 | def __init__(self): 61 | self.device = pyaudio.PyAudio() 62 | 63 | def __call__(self, *args, **kwargs): 64 | return StreamContext(self.device, *args, **kwargs) 65 | 66 | 67 | player = Player() 68 | 69 | 70 | async def speak(text): 71 | url, params = url_and_param_tts() 72 | 73 | params["tex"] = text 74 | 75 | with tempfile.NamedTemporaryFile(suffix=".wav") as audio_file: 76 | async with aiohttp.ClientSession() as session: 77 | async with session.post(url, data=params) as response: 78 | audio_data = await response.read() 79 | audio_file.write(audio_data) 80 | 81 | audio_file.file.seek(0) 82 | 83 | with wave.open(audio_file, "rb") as file: 84 | with player( 85 | file, 86 | format=player.device.get_format_from_width(file.getsampwidth()), 87 | channels=file.getnchannels(), 88 | rate=file.getframerate(), 89 | output=True, 90 | ) as stream: 91 | 92 | await asyncio.to_thread(stream.keep_play) 93 | -------------------------------------------------------------------------------- /gpt_bot/record.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import numpy 3 | import openai 4 | import pyaudio 5 | import tempfile 6 | import threading 7 | import wave 8 | 9 | #设置参数 10 | CHUNK = 1024 #每次读取的buffer 大小 11 | FORMAT = pyaudio.paInt16 12 | CHANNELS = 1 #单通道 13 | RATE = 16 * 1024 #采样率 14 | 15 | 16 | class StreamContext: 17 | 18 | def __init__(self, device, *args, **kwargs): 19 | self.device = device 20 | self.args = args 21 | self.kwargs = kwargs 22 | 23 | def __enter__(self): 24 | self.stream = self.device.open(*self.args, **self.kwargs) 25 | self.frames = [] 26 | self.keep_running = True 27 | return self 28 | 29 | def __exit__(self, *args): 30 | self.stream.stop_stream() 31 | self.stream.close() 32 | 33 | def keep_record(self): 34 | while self.keep_running: 35 | data = self.stream.read(CHUNK) 36 | self.frames.append(numpy.frombuffer(data, dtype=numpy.int16)) 37 | 38 | 39 | class Recorder: 40 | 41 | def __init__(self): 42 | self.device = pyaudio.PyAudio() 43 | 44 | def __call__(self, *args, **kwargs): 45 | return StreamContext(self.device, *args, **kwargs) 46 | 47 | 48 | recorder = Recorder() 49 | 50 | 51 | async def record(): 52 | with recorder(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK) as stream: 53 | print("recording... press enter to stop ", end="") 54 | 55 | t = threading.Thread(target=stream.keep_record) 56 | t.start() 57 | 58 | input_event = asyncio.Event() 59 | asyncio.get_event_loop().add_reader(0, input_event.set) 60 | await input_event.wait() 61 | input() 62 | 63 | stream.keep_running = False 64 | t.join() 65 | 66 | audio_data = numpy.concatenate(stream.frames, axis=0) 67 | return audio_data 68 | 69 | 70 | async def transcribe(audio_data): 71 | with tempfile.NamedTemporaryFile(suffix=".wav") as audio_file: 72 | print("transcribing... ", end="") 73 | 74 | with wave.open(audio_file, "wb") as wavfile: 75 | wavfile.setnchannels(1) # 设置声道数为1 76 | wavfile.setsampwidth(2) # 设置采样位数为16位 77 | wavfile.setframerate(RATE) # 设置采样率为RATE 78 | wavfile.writeframes(audio_data.tobytes()) # 将数据写入wav文件 79 | 80 | audio_file.file.seek(0) 81 | transcript = await openai.Audio.atranscribe("whisper-1", audio_file) 82 | 83 | print() 84 | return transcript.text 85 | 86 | 87 | async def record_and_transcribe(): 88 | return await transcribe(await record()) 89 | -------------------------------------------------------------------------------- /gpt_bot/__main__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import openai 3 | import os 4 | import prompt_toolkit 5 | import tiktoken 6 | 7 | # Some static configuration 8 | MODEL = "gpt-3.5-turbo" # Choose the ID of the model to use 9 | PROMPT = "You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible." 10 | MAX_TOKENS = 4096 11 | 12 | # Authenticate with OpenAI API 13 | assert "OPENAI_API_KEY" in os.environ, "OPENAI_API_KEY environment variable not set." 14 | openai.api_key = os.environ["OPENAI_API_KEY"] 15 | if "OPENAI_PROXY" in os.environ: 16 | openai.proxy = os.environ["OPENAI_PROXY"] 17 | 18 | 19 | def num_tokens_from_messages(messages, model): 20 | encoding = tiktoken.encoding_for_model(model) 21 | if model == MODEL: # note: future models may deviate from this 22 | num_tokens = 0 23 | for message in messages: 24 | num_tokens += 4 # every message follows {role/name}\n{content}\n 25 | for key, value in message.items(): 26 | num_tokens += len(encoding.encode(value)) 27 | if key == "name": # if there's a name, the role is omitted 28 | num_tokens += -1 # role is always required and always 1 token 29 | num_tokens += 2 # every reply is primed with assistant 30 | return num_tokens 31 | else: 32 | raise NotImplementedError(f"""num_tokens_from_messages() is not presently implemented for model {model}. 33 | See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens.""") 34 | 35 | 36 | def generate_messages(chat_history, model, prompt): 37 | assert len(chat_history) % 2 == 1 38 | messages = [{"role": "system", "content": prompt}] 39 | roles = ["user", "assistant"] 40 | role_id = 0 41 | for msg in chat_history: 42 | messages.append({"role": roles[role_id], "content": msg}) 43 | role_id = 1 - role_id 44 | while num_tokens_from_messages(messages, model) > MAX_TOKENS // 2: 45 | messages = [messages[0]] + messages[3:] 46 | return messages 47 | 48 | 49 | async def completion(chat_history, model, prompt): 50 | messages = generate_messages(chat_history, model, prompt) 51 | stream = await openai.ChatCompletion.acreate(model=model, messages=messages, stream=True) 52 | async for response in stream: 53 | obj = response['choices'][0] 54 | if obj['finish_reason'] is not None: 55 | assert not obj['delta'] 56 | if obj['finish_reason'] == 'length': 57 | yield ' [!Output truncated due to limit]' 58 | return 59 | if 'role' in obj['delta']: 60 | if obj['delta']['role'] != 'assistant': 61 | raise ValueError("Role error") 62 | if 'content' in obj['delta']: 63 | yield obj['delta']['content'] 64 | 65 | 66 | def prompt_continuation(width, line_number, is_soft_wrap): 67 | return '.' * (width - 1) + ' ' 68 | 69 | 70 | class App: 71 | 72 | def __init__(self, model=MODEL, prompt=PROMPT, history="~/.gpt_history"): 73 | self.model = model 74 | self.prompt = prompt 75 | 76 | self.middleware = {} 77 | 78 | self.multiline = False 79 | self.chat_history = [] 80 | self.session = prompt_toolkit.PromptSession(history=prompt_toolkit.history.FileHistory(os.path.expanduser(history))) 81 | self.speak = False 82 | 83 | async def driver(self): 84 | print(f"Welcome to the chatbot({self.model})! PROMPT is") 85 | print(self.prompt) 86 | print() 87 | 88 | while True: 89 | user_input = await self.session.prompt_async( 90 | "You: ", 91 | multiline=self.multiline, 92 | prompt_continuation=prompt_continuation, 93 | ) 94 | 95 | do_quit = False 96 | do_continue = False 97 | for prefix, function in self.middleware.items(): 98 | if user_input.startswith(prefix): 99 | try: 100 | line = user_input[len(prefix):] 101 | if asyncio.iscoroutinefunction(function): 102 | user_input = await function(self, line) 103 | else: 104 | user_input = function(self, line) 105 | break 106 | except self.Exit: 107 | do_quit = True 108 | break 109 | except self.Continue: 110 | do_continue = True 111 | break 112 | if do_quit: 113 | break 114 | if do_continue: 115 | continue 116 | 117 | self.chat_history.append(user_input) 118 | print("Bot: ", end="", flush=True) 119 | bot_response = "" 120 | # Get response from OpenAI's GPT-3 model 121 | async for message in completion(self.chat_history, self.model, self.prompt): 122 | print(message, end="", flush=True) 123 | bot_response += message 124 | print() 125 | if self.speak: 126 | from .speak import speak 127 | await speak(bot_response) 128 | self.chat_history.append(bot_response) 129 | 130 | def handle(self, prefix): 131 | 132 | def handle_for_prefix(function): 133 | self.middleware[prefix] = function 134 | return function 135 | 136 | return handle_for_prefix 137 | 138 | class Exit(BaseException): 139 | pass 140 | 141 | class Continue(BaseException): 142 | pass 143 | 144 | 145 | app = App() 146 | 147 | 148 | @app.handle("/quit") 149 | @app.handle("/exit") 150 | def _(self, line): 151 | raise self.Exit() 152 | 153 | 154 | @app.handle("/multiline") 155 | def _(self, line): 156 | self.multiline = not self.multiline 157 | print(f"{self.multiline=}") 158 | raise self.Continue() 159 | 160 | 161 | @app.handle("/prompt") 162 | def _(self, line): 163 | self.prompt = line 164 | print("Update prompt to:", self.prompt) 165 | raise self.Continue() 166 | 167 | 168 | @app.handle("/record") 169 | async def _(self, line): 170 | from .record import record_and_transcribe 171 | user_input = await record_and_transcribe() 172 | print("You:", user_input) 173 | return user_input 174 | 175 | 176 | @app.handle("/history") 177 | def _(self, line): 178 | print("History:") 179 | print("Sys:", self.prompt) 180 | for i, content in enumerate(self.chat_history): 181 | print("You:" if i % 2 == 0 else "Bot:", content) 182 | raise self.Continue() 183 | 184 | 185 | @app.handle("/rollback") 186 | def _(self, line): 187 | self.chat_history = self.chat_history[:-2] 188 | print("Rollback the history") 189 | raise self.Continue() 190 | 191 | 192 | @app.handle("/edit") 193 | async def _(self, line): 194 | last_chat = self.chat_history[-1] 195 | user_edit = await self.session.prompt_async( 196 | "Bot: ", 197 | multiline=self.multiline, 198 | prompt_continuation=prompt_continuation, 199 | default=last_chat, 200 | ) 201 | self.chat_history[-1] = user_edit 202 | raise self.Continue() 203 | 204 | 205 | @app.handle("/speak") 206 | def _(self, line): 207 | self.speak = not self.speak 208 | print(f"{self.speak=}") 209 | raise self.Continue() 210 | 211 | 212 | if __name__ == "__main__": 213 | asyncio.run(app.driver()) 214 | --------------------------------------------------------------------------------