Add support of real time voice transcription with Kokoro.
You can easily do it we websockets.
Here is a sample code:
importasyncioimportjsonimportnumpyasnpimportrequestsimportpyaudiofromkokoroimportKPipelineimporttorchimportwarningsfrompynputimportkeyboardimportthreading# Suppress warningswarnings.filterwarnings("ignore",category=UserWarning)warnings.filterwarnings("ignore",category=FutureWarning)# Global speed controlSPEECH_SPEED=1.4# Default speedSPEED_STEP=0.1# Speed adjustment incrementspeed_lock=threading.Lock()# Initialize Kokoro TTS pipelinepipeline=KPipeline(lang_code='a')# PyAudio configurationFORMAT=pyaudio.paFloat32CHANNELS=1RATE=24000CHUNK=1024# Initialize PyAudiopy_audio=pyaudio.PyAudio()audio_stream=py_audio.open(format=FORMAT,channels=CHANNELS,rate=RATE,output=True)defadjust_speed(direction):"""Adjust the speech speed up or down."""globalSPEECH_SPEEDwithspeed_lock:ifdirection=='up':SPEECH_SPEED=min(3.0,SPEECH_SPEED+SPEED_STEP)else:SPEECH_SPEED=max(0.5,SPEECH_SPEED-SPEED_STEP)print(f"\nSpeed adjusted to {SPEECH_SPEED:.1f}x")defon_press(key):"""Handle keyboard press events."""try:ifkey==keyboard.Key.up:adjust_speed('up')elifkey==keyboard.Key.down:adjust_speed('down')exceptAttributeError:passdefstart_keyboard_listener():"""Start the keyboard listener in a separate thread."""listener=keyboard.Listener(on_press=on_press)listener.start()defclean_text(text):"""Clean the text by removing unnecessary characters and fixing spaces."""text=text.replace('*','').strip()common_fixes={'emp ower':'empower','effect ively':'effectively','decision -m':'decision-m','decision - ':'decision-',' -':'-','- ':'-'}forwrong,correctincommon_fixes.items():text=text.replace(wrong,correct)text=' '.join(text.split())returntextdeftensor_to_audio_bytes(tensor_data):"""Convert a PyTorch tensor or numpy array to audio bytes."""ifisinstance(tensor_data,torch.Tensor):audio_np=tensor_data.cpu().numpy()elifisinstance(tensor_data,np.ndarray):audio_np=tensor_dataelse:raiseValueError(f"Unexpected data type: {type(tensor_data)}")audio_np=audio_np.astype(np.float32)ifaudio_np.max()>1.0oraudio_np.min()<-1.0:audio_np=np.clip(audio_np,-1.0,1.0)returnaudio_np.tobytes()defprocess_audio_chunk(audio_data):"""Process and play a single audio chunk."""ifaudio_dataisnotNoneandlen(audio_data)>0:try:audio_bytes=tensor_to_audio_bytes(audio_data)audio_stream.write(audio_bytes)exceptExceptionase:print(f"Error processing audio chunk: {e}")defprocess_text_chunk(text_chunk):"""Process a single text chunk through Kokoro TTS."""cleaned_text=clean_text(text_chunk)print(f"Processing: {cleaned_text}")try:withspeed_lock:current_speed=SPEECH_SPEEDgenerator=pipeline(cleaned_text,voice='af_nicole',speed=current_speed)for_,(_,_,audio)inenumerate(generator):ifaudioisnotNone:process_audio_chunk(audio)breakexceptExceptionase:print(f"Error processing chunk: {e}")defget_ollama_stream(prompt,model="deepseek-r1:1.5b"):"""Stream text from Ollama using HTTP streaming."""url="http://localhost:11434/api/generate"data={"model":model,"prompt":prompt}try:response=requests.post(url,json=data,stream=True)buffer=""forlineinresponse.iter_lines():ifline:json_response=json.loads(line)if'response'injson_response:chunk=json_response['response']buffer+=chunk# Check for sentence endingswhileTrue:sentence_end=-1forend_charin['.','!','?']:pos=buffer.find(end_char)ifpos!=-1and(sentence_end==-1orpos<sentence_end):sentence_end=posifsentence_end==-1:break# Extract the sentence and yield itsentence=buffer[:sentence_end+1].strip()ifsentence:yieldsentencebuffer=buffer[sentence_end+1:].strip()# Yield any remaining textifbuffer.strip():yieldbuffer.strip()exceptrequests.exceptions.RequestExceptionase:print(f"Error connecting to Ollama: {e}")returndefprocess_prompt(prompt):"""Process the entire prompt."""print(f"\nProcessing prompt: {prompt}")fortext_chunkinget_ollama_stream(prompt):process_text_chunk(text_chunk)defmain():print("Welcome to Ollama-Kokoro Real-time TTS!")print("Controls:")print("- Ctrl + Up Arrow: Increase speed")print("- Ctrl + Down Arrow: Decrease speed")print("- Type 'quit' or 'exit' to end the program")print(f"Current speed: {SPEECH_SPEED}x")# Start the keyboard listenerstart_keyboard_listener()try:whileTrue:prompt=input("\nEnter your prompt: ").strip()ifprompt.lower()in['quit','exit']:print("Goodbye!")breakifnotprompt:print("Please enter a valid prompt.")continueprocess_prompt(prompt)exceptKeyboardInterrupt:print("\nProgram interrupted by user. Goodbye!")finally:audio_stream.stop_stream()audio_stream.close()py_audio.terminate()if__name__=="__main__":main()
Originally created by @andsty on GitHub (Feb 15, 2025).
Original GitHub issue: https://github.com/open-webui/open-webui/issues/10064
# Feature Request
## Important Notes
Add support of real time voice transcription with Kokoro.
You can easily do it we websockets.
Here is a sample code:
```python
import asyncio
import json
import numpy as np
import requests
import pyaudio
from kokoro import KPipeline
import torch
import warnings
from pynput import keyboard
import threading
# Suppress warnings
warnings.filterwarnings("ignore", category=UserWarning)
warnings.filterwarnings("ignore", category=FutureWarning)
# Global speed control
SPEECH_SPEED = 1.4 # Default speed
SPEED_STEP = 0.1 # Speed adjustment increment
speed_lock = threading.Lock()
# Initialize Kokoro TTS pipeline
pipeline = KPipeline(lang_code='a')
# PyAudio configuration
FORMAT = pyaudio.paFloat32
CHANNELS = 1
RATE = 24000
CHUNK = 1024
# Initialize PyAudio
py_audio = pyaudio.PyAudio()
audio_stream = py_audio.open(format=FORMAT,
channels=CHANNELS,
rate=RATE,
output=True)
def adjust_speed(direction):
"""Adjust the speech speed up or down."""
global SPEECH_SPEED
with speed_lock:
if direction == 'up':
SPEECH_SPEED = min(3.0, SPEECH_SPEED + SPEED_STEP)
else:
SPEECH_SPEED = max(0.5, SPEECH_SPEED - SPEED_STEP)
print(f"\nSpeed adjusted to {SPEECH_SPEED:.1f}x")
def on_press(key):
"""Handle keyboard press events."""
try:
if key == keyboard.Key.up:
adjust_speed('up')
elif key == keyboard.Key.down:
adjust_speed('down')
except AttributeError:
pass
def start_keyboard_listener():
"""Start the keyboard listener in a separate thread."""
listener = keyboard.Listener(on_press=on_press)
listener.start()
def clean_text(text):
"""Clean the text by removing unnecessary characters and fixing spaces."""
text = text.replace('*', '').strip()
common_fixes = {
'emp ower': 'empower',
'effect ively': 'effectively',
'decision -m': 'decision-m',
'decision - ': 'decision-',
' -': '-',
'- ': '-'
}
for wrong, correct in common_fixes.items():
text = text.replace(wrong, correct)
text = ' '.join(text.split())
return text
def tensor_to_audio_bytes(tensor_data):
"""Convert a PyTorch tensor or numpy array to audio bytes."""
if isinstance(tensor_data, torch.Tensor):
audio_np = tensor_data.cpu().numpy()
elif isinstance(tensor_data, np.ndarray):
audio_np = tensor_data
else:
raise ValueError(f"Unexpected data type: {type(tensor_data)}")
audio_np = audio_np.astype(np.float32)
if audio_np.max() > 1.0 or audio_np.min() < -1.0:
audio_np = np.clip(audio_np, -1.0, 1.0)
return audio_np.tobytes()
def process_audio_chunk(audio_data):
"""Process and play a single audio chunk."""
if audio_data is not None and len(audio_data) > 0:
try:
audio_bytes = tensor_to_audio_bytes(audio_data)
audio_stream.write(audio_bytes)
except Exception as e:
print(f"Error processing audio chunk: {e}")
def process_text_chunk(text_chunk):
"""Process a single text chunk through Kokoro TTS."""
cleaned_text = clean_text(text_chunk)
print(f"Processing: {cleaned_text}")
try:
with speed_lock:
current_speed = SPEECH_SPEED
generator = pipeline(cleaned_text, voice='af_nicole', speed=current_speed)
for _, (_, _, audio) in enumerate(generator):
if audio is not None:
process_audio_chunk(audio)
break
except Exception as e:
print(f"Error processing chunk: {e}")
def get_ollama_stream(prompt, model="deepseek-r1:1.5b"):
"""Stream text from Ollama using HTTP streaming."""
url = "http://localhost:11434/api/generate"
data = {
"model": model,
"prompt": prompt
}
try:
response = requests.post(url, json=data, stream=True)
buffer = ""
for line in response.iter_lines():
if line:
json_response = json.loads(line)
if 'response' in json_response:
chunk = json_response['response']
buffer += chunk
# Check for sentence endings
while True:
sentence_end = -1
for end_char in ['.', '!', '?']:
pos = buffer.find(end_char)
if pos != -1 and (sentence_end == -1 or pos < sentence_end):
sentence_end = pos
if sentence_end == -1:
break
# Extract the sentence and yield it
sentence = buffer[:sentence_end + 1].strip()
if sentence:
yield sentence
buffer = buffer[sentence_end + 1:].strip()
# Yield any remaining text
if buffer.strip():
yield buffer.strip()
except requests.exceptions.RequestException as e:
print(f"Error connecting to Ollama: {e}")
return
def process_prompt(prompt):
"""Process the entire prompt."""
print(f"\nProcessing prompt: {prompt}")
for text_chunk in get_ollama_stream(prompt):
process_text_chunk(text_chunk)
def main():
print("Welcome to Ollama-Kokoro Real-time TTS!")
print("Controls:")
print("- Ctrl + Up Arrow: Increase speed")
print("- Ctrl + Down Arrow: Decrease speed")
print("- Type 'quit' or 'exit' to end the program")
print(f"Current speed: {SPEECH_SPEED}x")
# Start the keyboard listener
start_keyboard_listener()
try:
while True:
prompt = input("\nEnter your prompt: ").strip()
if prompt.lower() in ['quit', 'exit']:
print("Goodbye!")
break
if not prompt:
print("Please enter a valid prompt.")
continue
process_prompt(prompt)
except KeyboardInterrupt:
print("\nProgram interrupted by user. Goodbye!")
finally:
audio_stream.stop_stream()
audio_stream.close()
py_audio.terminate()
if __name__ == "__main__":
main()
```
The demos I've heard for Kokoro are fantastic and far more realistic than the existing TTS implementation.
<!-- gh-comment-id:2661027738 -->
@cyuzik commented on GitHub (Feb 15, 2025):
The demos I've heard for Kokoro are fantastic and far more realistic than the existing TTS implementation.
🎤 Kokoro-JS TTS Support: A new on-device, high-quality text-to-speech engine has been integrated, vastly improving voice generation quality—everything runs directly in your browser.
That's one option available. If your browser does not have WebGPU support, then the Kokoro.js option will not work for you.
A second option available is to run Kokoro-FastAPI locally and integrate that into your Open WebUI instance.
<!-- gh-comment-id:2661057724 -->
@silentoplayz commented on GitHub (Feb 15, 2025):
As of [v0.5.11](https://github.com/open-webui/open-webui/releases/tag/v0.5.11) of Open WebUI:
> 🎤 Kokoro-JS TTS Support: A new on-device, high-quality text-to-speech engine has been integrated, vastly improving voice generation quality—everything runs directly in your browser.
That's one option available. If your browser does not have [WebGPU support](https://caniuse.com/webgpu), then the Kokoro.js option will not work for you.
A second option available is to run [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI) locally and integrate that into your Open WebUI instance.
The kokoro integration is perfect. But as models like deepseek generate a lot of content i was thinking that with similar implemenration i shared it wont wait wait for inference to finish and then start the voice
<!-- gh-comment-id:2661061948 -->
@myrulezzz commented on GitHub (Feb 15, 2025):
The kokoro integration is perfect. But as models like deepseek generate a lot of content i was thinking that with similar implemenration i shared it wont wait wait for inference to finish and then start the voice
There is a setting in Admin Settings > Settings > Audio called "Response Splitting" which will dictate how quickly portions of the response will be sent to TTS engine.
<!-- gh-comment-id:2661095026 -->
@StevePierce commented on GitHub (Feb 15, 2025):
There is a setting in Admin Settings > Settings > Audio called "Response Splitting" which will dictate how quickly portions of the response will be sent to TTS engine.
But even that one having auto audio response enabled for llm answers it will wait for the response to finish and then start.it will not start lets say when the first paragraph is finished. So if there is a long response, the response should finish and then start the audio.
<!-- gh-comment-id:2661096417 -->
@myrulezzz commented on GitHub (Feb 15, 2025):
But even that one having auto audio response enabled for llm answers it will wait for the response to finish and then start.it will not start lets say when the first paragraph is finished. So if there is a long response, the response should finish and then start the audio.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Originally created by @andsty on GitHub (Feb 15, 2025).
Original GitHub issue: https://github.com/open-webui/open-webui/issues/10064
Feature Request
Important Notes
Add support of real time voice transcription with Kokoro.
You can easily do it we websockets.
Here is a sample code:
@cyuzik commented on GitHub (Feb 15, 2025):
The demos I've heard for Kokoro are fantastic and far more realistic than the existing TTS implementation.
@silentoplayz commented on GitHub (Feb 15, 2025):
As of v0.5.11 of Open WebUI:
That's one option available. If your browser does not have WebGPU support, then the Kokoro.js option will not work for you.
A second option available is to run Kokoro-FastAPI locally and integrate that into your Open WebUI instance.
@myrulezzz commented on GitHub (Feb 15, 2025):
The kokoro integration is perfect. But as models like deepseek generate a lot of content i was thinking that with similar implemenration i shared it wont wait wait for inference to finish and then start the voice
@StevePierce commented on GitHub (Feb 15, 2025):
There is a setting in Admin Settings > Settings > Audio called "Response Splitting" which will dictate how quickly portions of the response will be sent to TTS engine.
@myrulezzz commented on GitHub (Feb 15, 2025):
But even that one having auto audio response enabled for llm answers it will wait for the response to finish and then start.it will not start lets say when the first paragraph is finished. So if there is a long response, the response should finish and then start the audio.
@tjbck commented on GitHub (Feb 16, 2025):
Already implemented.