This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

pkgs: init kokoro with streaming and download patch

+207
+54
5pkgs/kokoro/package.nix
··· 1 + { 2 + python3, 3 + fetchurl, 4 + fetchFromGitHub, 5 + }: 6 + let 7 + # Create a python environment with our overrides 8 + python = python3.override { 9 + self = python; 10 + packageOverrides = final: prev: { 11 + # Override kokoro with a more recent version and stdout support 12 + kokoro = prev.kokoro.overridePythonAttrs (old: { 13 + version = "0-unstable-2025-08-06"; 14 + src = fetchFromGitHub { 15 + owner = "hexgrad"; 16 + repo = "kokoro"; 17 + rev = "dfb907a02bba8152ca444717ca5d78747ccb4bec"; 18 + hash = "sha256-GJlc3+RCeYaAvojFFjK22nitDTWFWp6dAPJakw+//j8="; 19 + }; 20 + patches = (old.patches or [ ]) ++ [ 21 + ./stdout-support.patch 22 + ]; 23 + }); 24 + # Spacy model - downloaded and packaged as a python package 25 + spacy-en-core-web-sm = final.buildPythonPackage rec { 26 + pname = "en_core_web_sm"; 27 + version = "3.8.0"; 28 + format = "wheel"; 29 + 30 + src = fetchurl { 31 + url = "https://github.com/explosion/spacy-models/releases/download/${pname}-${version}/${pname}-${version}-py3-none-any.whl"; 32 + hash = "sha256-GTJCnbcn1L/z3u1rNM/AXfF3lPSlLusmz4ko98Gg+4U="; 33 + }; 34 + 35 + propagatedBuildInputs = [ final.spacy ]; 36 + 37 + pythonImportsCheck = [ "en_core_web_sm" ]; 38 + }; 39 + 40 + # Create a patched misaki that doesn't try to download spacy models 41 + misaki = prev.misaki.overridePythonAttrs (old: { 42 + postPatch = (old.postPatch or "") + '' 43 + # Patch out the spacy model download - we provide it as a dependency instead 44 + substituteInPlace misaki/en.py \ 45 + --replace-fail "if not spacy.util.is_package(name):" "if False:" \ 46 + --replace-fail "spacy.cli.download(name)" "pass" 47 + ''; 48 + 49 + propagatedBuildInputs = (old.propagatedBuildInputs or [ ]) ++ [ final.spacy-en-core-web-sm ]; 50 + }); 51 + }; 52 + }; 53 + in 54 + python.pkgs.kokoro
+153
5pkgs/kokoro/stdout-support.patch
··· 1 + --- a/kokoro/__main__.py 2025-12-21 20:28:56.595838972 +0100 2 + +++ b/kokoro/__main__.py 2025-12-21 20:50:06.842504063 +0100 3 + @@ -3,7 +3,10 @@ 4 + python3 -m kokoro --text "The sky above the port was the color of television, tuned to a dead channel." -o file.wav --debug 5 + 6 + echo "Bom dia mundo, como vão vocês" > text.txt 7 + -python3 -m kokoro -i text.txt -l p --voice pm_alex > audio.wav 8 + +python3 -m kokoro -i text.txt -l p --voice pm_alex -o output.wav 9 + + 10 + +# Streaming from stdin to stdout (pipe to mpv): 11 + +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 - 12 + 13 + Common issues: 14 + pip not installed: `uv pip install pip` 15 + @@ -13,6 +16,7 @@ 16 + """ 17 + 18 + import argparse 19 + +import sys 20 + import wave 21 + from pathlib import Path 22 + from typing import Generator, TYPE_CHECKING 23 + @@ -47,22 +51,58 @@ 24 + yield from pipeline(text, voice=voice, speed=speed, split_pattern=r"\n+") 25 + 26 + 27 + +def write_wav_audio(wav_file, text: str, kokoro_language: str, voice: str, speed=1) -> None: 28 + + """Write audio frames to an open wav file handle.""" 29 + + wav_file.setnchannels(1) # Mono audio 30 + + wav_file.setsampwidth(2) # 2 bytes per sample (16-bit audio) 31 + + wav_file.setframerate(24000) # Sample rate 32 + + 33 + + for result in generate_audio( 34 + + text, kokoro_language=kokoro_language, voice=voice, speed=speed 35 + + ): 36 + + logger.debug(result.phonemes) 37 + + if result.audio is None: 38 + + continue 39 + + audio_bytes = (result.audio.numpy() * 32767).astype(np.int16).tobytes() 40 + + wav_file.writeframes(audio_bytes) 41 + + 42 + + 43 + def generate_and_save_audio( 44 + - output_file: Path, text: str, kokoro_language: str, voice: str, speed=1 45 + + output_file: str, text: str, kokoro_language: str, voice: str, speed=1 46 + +) -> None: 47 + + if output_file == "-": 48 + + # Write to stdout 49 + + with wave.open(sys.stdout.buffer, "wb") as wav_file: 50 + + write_wav_audio(wav_file, text, kokoro_language, voice, speed) 51 + + else: 52 + + with wave.open(str(Path(output_file).resolve()), "wb") as wav_file: 53 + + write_wav_audio(wav_file, text, kokoro_language, voice, speed) 54 + + 55 + + 56 + +def stream_audio_raw( 57 + + input_stream, kokoro_language: str, voice: str, speed: float 58 + ) -> None: 59 + - with wave.open(str(output_file.resolve()), "wb") as wav_file: 60 + - wav_file.setnchannels(1) # Mono audio 61 + - wav_file.setsampwidth(2) # 2 bytes per sample (16-bit audio) 62 + - wav_file.setframerate(24000) # Sample rate 63 + - 64 + - for result in generate_audio( 65 + - text, kokoro_language=kokoro_language, voice=voice, speed=speed 66 + - ): 67 + + """Stream audio as raw PCM s16le to stdout, processing stdin line by line.""" 68 + + from kokoro import KPipeline 69 + + 70 + + if not voice.startswith(kokoro_language): 71 + + logger.warning(f"Voice {voice} is not made for language {kokoro_language}") 72 + + 73 + + pipeline = KPipeline(lang_code=kokoro_language) 74 + + output = sys.stdout.buffer 75 + + 76 + + for line in input_stream: 77 + + line = line.rstrip('\n\r') 78 + + if not line: 79 + + continue 80 + + logger.debug(f"Processing line: {line!r}") 81 + + for result in pipeline(line, voice=voice, speed=speed, split_pattern=r"\n+"): 82 + logger.debug(result.phonemes) 83 + if result.audio is None: 84 + continue 85 + audio_bytes = (result.audio.numpy() * 32767).astype(np.int16).tobytes() 86 + - wav_file.writeframes(audio_bytes) 87 + + output.write(audio_bytes) 88 + + output.flush() 89 + 90 + 91 + def main() -> None: 92 + @@ -83,8 +123,8 @@ 93 + "-o", 94 + "--output-file", 95 + "--output_file", 96 + - type=Path, 97 + - help="Path to output WAV file", 98 + + type=str, 99 + + help="Path to output WAV file (use - for stdout)", 100 + required=True, 101 + ) 102 + parser.add_argument( 103 + @@ -107,6 +147,11 @@ 104 + help="Speech speed", 105 + ) 106 + parser.add_argument( 107 + + "--stream", 108 + + action="store_true", 109 + + help="Stream mode: read stdin line-by-line, output raw PCM s16le 24kHz mono (for piping to mpv)", 110 + + ) 111 + + parser.add_argument( 112 + "--debug", 113 + action="store_true", 114 + help="Print DEBUG messages to console", 115 + @@ -118,6 +163,20 @@ 116 + 117 + lang = args.language or args.voice[0] 118 + 119 + + # Streaming mode: read stdin line by line, output raw PCM 120 + + if args.stream: 121 + + if args.output_file != "-": 122 + + logger.warning("Streaming mode always outputs to stdout, ignoring output file") 123 + + if args.text: 124 + + raise Exception("Cannot use --text with --stream, use stdin") 125 + + if args.input_file: 126 + + with open(args.input_file) as f: 127 + + stream_audio_raw(f, lang, args.voice, args.speed) 128 + + else: 129 + + stream_audio_raw(sys.stdin, lang, args.voice, args.speed) 130 + + return 131 + + 132 + + # Non-streaming mode: read all input, output WAV 133 + if args.text is not None and args.input_file is not None: 134 + raise Exception("You cannot specify both 'text' and 'input_file'") 135 + elif args.text: 136 + @@ -126,14 +185,13 @@ 137 + file: Path = args.input_file 138 + text = file.read_text() 139 + else: 140 + - import sys 141 + - print("Press Ctrl+D to stop reading input and start generating", flush=True) 142 + + print("Press Ctrl+D to stop reading input and start generating", file=sys.stderr, flush=True) 143 + text = '\n'.join(sys.stdin) 144 + 145 + logger.debug(f"Input text: {text!r}") 146 + 147 + - out_file: Path = args.output_file 148 + - if not out_file.suffix == ".wav": 149 + + out_file: str = args.output_file 150 + + if out_file != "-" and not out_file.endswith(".wav"): 151 + logger.warning("The output file name should end with .wav") 152 + generate_and_save_audio( 153 + output_file=out_file,