--- a/kokoro/__main__.py 2025-12-21 20:28:56.595838972 +0100 +++ b/kokoro/__main__.py 2025-12-21 20:50:06.842504063 +0100 @@ -3,7 +3,10 @@ python3 -m kokoro --text "The sky above the port was the color of television, tuned to a dead channel." -o file.wav --debug echo "Bom dia mundo, como vão vocês" > text.txt -python3 -m kokoro -i text.txt -l p --voice pm_alex > audio.wav +python3 -m kokoro -i text.txt -l p --voice pm_alex -o output.wav + +# Streaming from stdin to stdout (pipe to mpv): +ollama run llama3 "tell me a joke" | python3 -m kokoro -o - --stream | mpv --demuxer=rawaudio --demuxer-rawaudio-rate=24000 --demuxer-rawaudio-format=s16le --demuxer-rawaudio-channels=1 - Common issues: pip not installed: `uv pip install pip` @@ -13,6 +16,7 @@ """ import argparse +import sys import wave from pathlib import Path from typing import Generator, TYPE_CHECKING @@ -47,22 +51,58 @@ yield from pipeline(text, voice=voice, speed=speed, split_pattern=r"\n+") +def write_wav_audio(wav_file, text: str, kokoro_language: str, voice: str, speed=1) -> None: + """Write audio frames to an open wav file handle.""" + wav_file.setnchannels(1) # Mono audio + wav_file.setsampwidth(2) # 2 bytes per sample (16-bit audio) + wav_file.setframerate(24000) # Sample rate + + for result in generate_audio( + text, kokoro_language=kokoro_language, voice=voice, speed=speed + ): + logger.debug(result.phonemes) + if result.audio is None: + continue + audio_bytes = (result.audio.numpy() * 32767).astype(np.int16).tobytes() + wav_file.writeframes(audio_bytes) + + def generate_and_save_audio( - output_file: Path, text: str, kokoro_language: str, voice: str, speed=1 + output_file: str, text: str, kokoro_language: str, voice: str, speed=1 +) -> None: + if output_file == "-": + # Write to stdout + with wave.open(sys.stdout.buffer, "wb") as wav_file: + write_wav_audio(wav_file, text, kokoro_language, voice, speed) + else: + with wave.open(str(Path(output_file).resolve()), "wb") as wav_file: + write_wav_audio(wav_file, text, kokoro_language, voice, speed) + + +def stream_audio_raw( + input_stream, kokoro_language: str, voice: str, speed: float ) -> None: - with wave.open(str(output_file.resolve()), "wb") as wav_file: - wav_file.setnchannels(1) # Mono audio - wav_file.setsampwidth(2) # 2 bytes per sample (16-bit audio) - wav_file.setframerate(24000) # Sample rate - - for result in generate_audio( - text, kokoro_language=kokoro_language, voice=voice, speed=speed - ): + """Stream audio as raw PCM s16le to stdout, processing stdin line by line.""" + from kokoro import KPipeline + + if not voice.startswith(kokoro_language): + logger.warning(f"Voice {voice} is not made for language {kokoro_language}") + + pipeline = KPipeline(lang_code=kokoro_language) + output = sys.stdout.buffer + + for line in input_stream: + line = line.rstrip('\n\r') + if not line: + continue + logger.debug(f"Processing line: {line!r}") + for result in pipeline(line, voice=voice, speed=speed, split_pattern=r"\n+"): logger.debug(result.phonemes) if result.audio is None: continue audio_bytes = (result.audio.numpy() * 32767).astype(np.int16).tobytes() - wav_file.writeframes(audio_bytes) + output.write(audio_bytes) + output.flush() def main() -> None: @@ -83,8 +123,8 @@ "-o", "--output-file", "--output_file", - type=Path, - help="Path to output WAV file", + type=str, + help="Path to output WAV file (use - for stdout)", required=True, ) parser.add_argument( @@ -107,6 +147,11 @@ help="Speech speed", ) parser.add_argument( + "--stream", + action="store_true", + help="Stream mode: read stdin line-by-line, output raw PCM s16le 24kHz mono (for piping to mpv)", + ) + parser.add_argument( "--debug", action="store_true", help="Print DEBUG messages to console", @@ -118,6 +163,20 @@ lang = args.language or args.voice[0] + # Streaming mode: read stdin line by line, output raw PCM + if args.stream: + if args.output_file != "-": + logger.warning("Streaming mode always outputs to stdout, ignoring output file") + if args.text: + raise Exception("Cannot use --text with --stream, use stdin") + if args.input_file: + with open(args.input_file) as f: + stream_audio_raw(f, lang, args.voice, args.speed) + else: + stream_audio_raw(sys.stdin, lang, args.voice, args.speed) + return + + # Non-streaming mode: read all input, output WAV if args.text is not None and args.input_file is not None: raise Exception("You cannot specify both 'text' and 'input_file'") elif args.text: @@ -126,14 +185,13 @@ file: Path = args.input_file text = file.read_text() else: - import sys - print("Press Ctrl+D to stop reading input and start generating", flush=True) + print("Press Ctrl+D to stop reading input and start generating", file=sys.stderr, flush=True) text = '\n'.join(sys.stdin) logger.debug(f"Input text: {text!r}") - out_file: Path = args.output_file - if not out_file.suffix == ".wav": + out_file: str = args.output_file + if out_file != "-" and not out_file.endswith(".wav"): logger.warning("The output file name should end with .wav") generate_and_save_audio( output_file=out_file,