Skip to content

Commit 599db30

Browse files
openvino-agentrkazantsRyanMetcalfeInt8
authored
[OpenVINO] Support Kokoro model (#1653)
* Support Kokoro TTS model * Save misaki data files * Fix inference * Add preprocess_input * kokoro: fix kokoro export from local model directory * modeling_text2speech.py: fix Kokoro preprocessing/generation flow to work reliably across long prompts and non-English languages * kokoro: download misaka data files even when exporting from local dir * Fix inference and export * Add test for export cli * Apply code formatting * Fix test_export tests * Remove fallback for tiny model creation * Fix test_exporters_cli * Add inference tests * Applied commend from review * Apply code-formatting * Apply code formatting --------- Co-authored-by: Kazantsev, Roman <roman.kazantsev@intel.com> Co-authored-by: Ryan Metcalfe <ryan.metcalfe@intel.com>
1 parent d513dfe commit 599db30

15 files changed

Lines changed: 919 additions & 15 deletions

File tree

docs/source/openvino/models.mdx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,7 @@ Here is the list of the supported architectures :
191191
- All Transformer and CLIP-based models.
192192

193193
## [OpenCLIP](https://github.com/mlfoundations/open_clip)
194-
- All CLIP-based models
194+
- All CLIP-based models
195+
196+
## [Kokoro](https://github.com/hexgrad/kokoro)
197+
- Kokoro-82M (text-to-speech)

optimum/commands/export/openvino.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def parse_args_openvino(parser: "ArgumentParser"):
8585
optional_group.add_argument(
8686
"--library",
8787
type=str,
88-
choices=["transformers", "diffusers", "timm", "sentence_transformers", "open_clip"],
88+
choices=["transformers", "diffusers", "timm", "sentence_transformers", "open_clip", "kokoro"],
8989
default=None,
9090
help="The library used to load the model before export. If not provided, will attempt to infer the local checkpoint's library",
9191
)

optimum/exporters/openvino/__main__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
)
4040
from optimum.intel.utils.modeling_utils import (
4141
_infer_library_from_model_name_or_path,
42+
_KokoroForTextToSpeech,
4243
_OpenClipForZeroShotImageClassification,
4344
)
4445

@@ -86,6 +87,8 @@ def infer_task(
8687
if task == "auto":
8788
if library_name == "open_clip":
8889
task = "zero-shot-image-classification"
90+
elif library_name == "kokoro":
91+
task = "text-to-audio"
8992
else:
9093
try:
9194
task = TasksManager._infer_task_from_model_name_or_path(
@@ -478,6 +481,8 @@ def bitnet_load_hook(self, state_dict, prefix, *args, **kwargs):
478481
try:
479482
if library_name == "open_clip":
480483
model = _OpenClipForZeroShotImageClassification.from_pretrained(model_name_or_path, cache_dir=cache_dir)
484+
elif library_name == "kokoro":
485+
model = _KokoroForTextToSpeech.from_pretrained(model_name_or_path, cache_dir=cache_dir, token=token)
481486
else:
482487
# remote code models like phi3_v internvl2, minicpmv, internvl2, nanollava, maira2 should be loaded using AutoModelForCausalLM and not AutoModelForImageTextToText
483488
# TODO: use config.auto_map to load remote code models instead (for other models we can directly use config.architectures)

optimum/exporters/openvino/convert.py

Lines changed: 109 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
)
4242
from optimum.intel.utils.import_utils import (
4343
_diffusers_version,
44+
_kokoro_version,
4445
_nncf_version,
4546
_open_clip_version,
4647
_optimum_intel_version,
@@ -67,6 +68,7 @@
6768
OV_XML_FILE_NAME,
6869
_get_dynamic_shapes_info,
6970
_get_input_info,
71+
_get_kokoro_submodels_fn_and_export_configs,
7072
_get_model_dtype,
7173
_get_open_clip_submodels_fn_and_export_configs,
7274
_normalize_dummy_inputs,
@@ -552,6 +554,92 @@ def export_models(
552554
return outputs
553555

554556

557+
def _save_kokoro_config_and_assets(model, output: Path):
558+
"""Save Kokoro model config.json and export voice embeddings."""
559+
import json
560+
import tempfile
561+
import urllib.request
562+
563+
import numpy as np
564+
from huggingface_hub import hf_hub_download, list_repo_files
565+
566+
repo_id = getattr(model, "_kokoro_repo_id", None)
567+
568+
# Save config.json
569+
config_dict = {}
570+
for key in vars(model.config):
571+
if not key.startswith("_"):
572+
config_dict[key] = getattr(model.config, key)
573+
config_path = output / "config.json"
574+
with open(config_path, "w", encoding="utf-8") as f:
575+
json.dump(config_dict, f, indent=2)
576+
577+
# Save misaki data files from GitHub to data dir of output directory
578+
try:
579+
MISAKI_DATA_URL = "https://raw.githubusercontent.com/hexgrad/misaki/main/misaki/data"
580+
MISAKI_DATA_FILES = [
581+
"gb_gold.json",
582+
"gb_silver.json",
583+
"us_gold.json",
584+
"us_silver.json",
585+
"vi_acronyms.json",
586+
"vi_symbols.json",
587+
"vi_teencode.json",
588+
"ja_words.txt",
589+
]
590+
data_out = output / "data"
591+
data_out.mkdir(parents=True, exist_ok=True)
592+
for fname in MISAKI_DATA_FILES:
593+
url = f"{MISAKI_DATA_URL}/{fname}"
594+
dest = data_out / fname
595+
try:
596+
urllib.request.urlretrieve(url, dest)
597+
logger.info(f"Downloaded misaki data file: {fname}")
598+
except Exception as e:
599+
logger.warning(f"Failed to download {fname} from {url}: {e}")
600+
except Exception as e:
601+
logger.warning(f"Could not download misaki data files: {e}")
602+
603+
if repo_id is None:
604+
return
605+
606+
# Export voice embeddings to .bin format
607+
voices_dir = output / "voices"
608+
voices_dir.mkdir(parents=True, exist_ok=True)
609+
610+
try:
611+
repo_files = list_repo_files(repo_id=repo_id)
612+
except Exception:
613+
logger.warning(f"Could not list files for {repo_id}. Skipping voice export.")
614+
return
615+
616+
voice_pt_files = sorted(path for path in repo_files if path.startswith("voices/") and path.endswith(".pt"))
617+
if not voice_pt_files:
618+
return
619+
620+
logger.info(f"Found {len(voice_pt_files)} voice files. Exporting to {voices_dir} ...")
621+
with tempfile.TemporaryDirectory(prefix="kokoro_voice_pt_") as tmp_dir:
622+
for remote_path in voice_pt_files:
623+
local_pt = hf_hub_download(repo_id=repo_id, filename=remote_path, local_dir=tmp_dir)
624+
voice_name = Path(remote_path).stem
625+
out_bin = voices_dir / f"{voice_name}.bin"
626+
627+
import torch
628+
629+
voice_obj = torch.load(local_pt, map_location="cpu")
630+
if torch.is_tensor(voice_obj):
631+
voice_tensor = voice_obj
632+
elif isinstance(voice_obj, dict):
633+
voice_tensor = next(v for v in voice_obj.values() if torch.is_tensor(v))
634+
else:
635+
logger.warning(f"Unsupported voice format in {remote_path}, skipping.")
636+
continue
637+
638+
voice_tensor = voice_tensor.detach().cpu().to(torch.float32).contiguous()
639+
np.asarray(voice_tensor.numpy(), dtype=np.float32).tofile(out_bin)
640+
logger.info(f"Exported {remote_path} -> {out_bin}")
641+
642+
555643
def export_from_model(
556644
model: Union["PreTrainedModel", "ModelMixin", "DiffusionPipeline"],
557645
output: Union[str, Path],
@@ -576,7 +664,7 @@ def export_from_model(
576664
)
577665

578666
library_name = _infer_library_from_model_or_model_class(model)
579-
if library_name != "open_clip":
667+
if library_name not in ("open_clip", "kokoro"):
580668
TasksManager.standardize_model_attributes(model)
581669

582670
if hasattr(model.config, "export_model_type") and model.config.export_model_type is not None:
@@ -594,12 +682,15 @@ def export_from_model(
594682
if task is not None and task != "auto":
595683
task = TasksManager.map_from_synonym(task)
596684
else:
597-
try:
598-
task = TasksManager._infer_task_from_model_or_model_class(model=model)
599-
except (ValueError, KeyError) as e:
600-
raise RuntimeError(
601-
f"The model task could not be automatically inferred in `export_from_model`. Please provide the argument `task` with the relevant task from {', '.join(TasksManager.get_all_tasks())}. Detailed error: {e}"
602-
)
685+
if library_name == "kokoro":
686+
task = "text-to-audio"
687+
else:
688+
try:
689+
task = TasksManager._infer_task_from_model_or_model_class(model=model)
690+
except (ValueError, KeyError) as e:
691+
raise RuntimeError(
692+
f"The model task could not be automatically inferred in `export_from_model`. Please provide the argument `task` with the relevant task from {', '.join(TasksManager.get_all_tasks())}. Detailed error: {e}"
693+
)
603694

604695
if (
605696
not custom_architecture
@@ -661,6 +752,12 @@ def export_from_model(
661752
model, library_name, task, preprocessors, custom_export_configs, fn_get_submodels
662753
)
663754

755+
if library_name == "kokoro":
756+
custom_architecture = True
757+
custom_export_configs, fn_get_submodels = _get_kokoro_submodels_fn_and_export_configs(
758+
model, library_name, task, preprocessors, custom_export_configs, fn_get_submodels
759+
)
760+
664761
if library_name == "diffusers":
665762
export_config, models_and_export_configs = get_diffusion_models_for_export_ext(model, exporter="openvino")
666763
stateful_submodels = False
@@ -695,6 +792,9 @@ def export_from_model(
695792
if hasattr(preprocess, "save_pretrained"):
696793
preprocess.save_pretrained(output)
697794

795+
files_subpaths = ["openvino_" + model_name + ".xml" for model_name in models_and_export_configs.keys()]
796+
elif library_name == "kokoro":
797+
_save_kokoro_config_and_assets(model, output)
698798
files_subpaths = ["openvino_" + model_name + ".xml" for model_name in models_and_export_configs.keys()]
699799
elif library_name != "diffusers":
700800
if is_transformers_version("<", "5"):
@@ -889,6 +989,8 @@ def _add_version_info_to_model(model: Model, library_name: Optional[str] = None)
889989
model.set_rt_info(_timm_version, ["optimum", "timm_version"])
890990
elif library_name == "open_clip":
891991
model.set_rt_info(_open_clip_version, ["optimum", "open_clip_version"])
992+
elif library_name == "kokoro":
993+
model.set_rt_info(_kokoro_version, ["optimum", "kokoro_version"])
892994
rt_info = model.get_rt_info()
893995
if "nncf" in rt_info:
894996
model.set_rt_info(_nncf_version, ["optimum", "nncf_version"])

optimum/exporters/openvino/model_configs.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@
171171
InternVL2ChatLangModelPatcher,
172172
InternVLChatImageEmbeddingModelPatcher,
173173
JaisModelPatcher,
174+
KokoroModelPatcher,
174175
Lfm2ModelPatcher,
175176
Lfm2MoeModelPatcher,
176177
Llama4ImageEmbeddingsModelPatcher,
@@ -263,6 +264,21 @@ def _warn_potential_accuracy_issue_ov_2026_1(model_type: str, min_transformers_v
263264
def init_model_configs():
264265
if "open_clip" not in TasksManager._LIBRARY_TO_SUPPORTED_MODEL_TYPES:
265266
TasksManager._LIBRARY_TO_SUPPORTED_MODEL_TYPES["open_clip"] = {}
267+
if "kokoro" not in TasksManager._LIBRARY_TO_SUPPORTED_MODEL_TYPES:
268+
TasksManager._LIBRARY_TO_SUPPORTED_MODEL_TYPES["kokoro"] = {}
269+
if "kokoro" not in TasksManager._LIBRARY_TO_TASKS_TO_MODEL_LOADER_MAP:
270+
from optimum.intel.utils.modeling_utils import _KokoroForTextToSpeech
271+
272+
try:
273+
import kokoro as _kokoro_module
274+
275+
if not hasattr(_kokoro_module, "_KokoroForTextToSpeech"):
276+
_kokoro_module._KokoroForTextToSpeech = _KokoroForTextToSpeech
277+
TasksManager._LIBRARY_TO_TASKS_TO_MODEL_LOADER_MAP["kokoro"] = {
278+
"text-to-audio": "_KokoroForTextToSpeech",
279+
}
280+
except ImportError:
281+
pass
266282

267283
TasksManager._CUSTOM_CLASSES[("pt", "phi4mm", "image-text-to-text")] = ("transformers", "AutoModelForCausalLM")
268284
TasksManager._CUSTOM_CLASSES[("pt", "phi4mm", "automatic-speech-recognition")] = (
@@ -6434,3 +6450,83 @@ def outputs(self) -> Dict[str, Dict[int, str]]:
64346450
"qwen3_5_moe_text", self._orig_config.text_config, self.int_dtype, self.float_dtype
64356451
).outputs
64366452
return super().outputs
6453+
6454+
6455+
class DummyKokoroInputGenerator(DummyInputGenerator):
6456+
"""Generates dummy inputs for the Kokoro TTS model."""
6457+
6458+
SUPPORTED_INPUT_NAMES = ("input_ids", "ref_s", "speed")
6459+
6460+
def __init__(
6461+
self,
6462+
task: str,
6463+
normalized_config: NormalizedConfig,
6464+
sequence_length: int = DEFAULT_DUMMY_SHAPES["sequence_length"],
6465+
**kwargs,
6466+
):
6467+
self.task = task
6468+
self.batch_size = 1
6469+
self.sequence_length = sequence_length
6470+
self.style_dim = getattr(normalized_config, "style_dim", 128)
6471+
6472+
def generate(self, input_name: str, framework: str = "pt", int_dtype: str = "int64", float_dtype: str = "fp32"):
6473+
if input_name == "input_ids":
6474+
shape = [self.batch_size, self.sequence_length]
6475+
input_ids_value = self.random_int_tensor(
6476+
shape=shape, min_value=0, max_value=178, framework=framework, dtype=int_dtype
6477+
)
6478+
input_ids_value[:, 0] = 0
6479+
input_ids_value[:, -1] = 0
6480+
return input_ids_value
6481+
elif input_name == "ref_s":
6482+
shape = [self.batch_size, self.style_dim * 2]
6483+
return self.random_float_tensor(
6484+
shape=shape, min_value=-1, max_value=1, framework=framework, dtype=float_dtype
6485+
)
6486+
elif input_name == "speed":
6487+
return self.random_int_tensor(shape=[1], min_value=1, max_value=10, framework=framework, dtype=float_dtype)
6488+
else:
6489+
raise ValueError(f"Unsupported input {input_name} for DummyKokoroInputGenerator")
6490+
6491+
6492+
@register_in_tasks_manager(
6493+
"kokoro",
6494+
*["text-to-audio"],
6495+
library_name="kokoro",
6496+
)
6497+
class KokoroOpenVINOConfig(OnnxConfig):
6498+
DEFAULT_ONNX_OPSET = 14
6499+
DUMMY_INPUT_GENERATOR_CLASSES = (DummyKokoroInputGenerator,)
6500+
NORMALIZED_CONFIG_CLASS = NormalizedConfig
6501+
_MODEL_PATCHER = KokoroModelPatcher
6502+
6503+
def __init__(
6504+
self,
6505+
config: "PretrainedConfig",
6506+
task: str = "text-to-audio",
6507+
int_dtype: str = "int64",
6508+
float_dtype: str = "fp32",
6509+
preprocessors: Optional[List[Any]] = None,
6510+
):
6511+
super().__init__(
6512+
config=config,
6513+
task=task,
6514+
int_dtype=int_dtype,
6515+
float_dtype=float_dtype,
6516+
preprocessors=preprocessors,
6517+
)
6518+
6519+
@property
6520+
def inputs(self) -> Dict[str, Dict[int, str]]:
6521+
return {
6522+
"input_ids": {1: ("sequence_length", 2, -1)},
6523+
"ref_s": {1: "style_dim"},
6524+
"speed": {},
6525+
}
6526+
6527+
@property
6528+
def outputs(self) -> Dict[str, Dict[int, str]]:
6529+
return {
6530+
"waveform": {0: "batch_size", 1: "audio_length"},
6531+
"phonemes": {0: "batch_size", 1: "phoneme_length"},
6532+
}

optimum/exporters/openvino/model_patcher.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9504,3 +9504,20 @@ def __exit__(self, exc_type, exc_value, traceback):
95049504
if isinstance(decoder_layer.mlp, Qwen3_5MoeSparseMoeBlock):
95059505
sparse_moe_block = decoder_layer.mlp
95069506
sparse_moe_block.forward = sparse_moe_block._orig_forward
9507+
9508+
9509+
class KokoroModelPatcher(ModelPatcher):
9510+
"""
9511+
Patches the Kokoro TTS model for OpenVINO export by redirecting forward
9512+
to forward_with_tokens, which takes (input_ids, ref_s, speed) and returns
9513+
(audio_waveform, phonemes).
9514+
"""
9515+
9516+
def __enter__(self):
9517+
super().__enter__()
9518+
self._model._orig_forward = self._model.forward
9519+
self._model.forward = self._model.forward_with_tokens
9520+
9521+
def __exit__(self, exc_type, exc_value, traceback):
9522+
super().__exit__(exc_type, exc_value, traceback)
9523+
self._model.forward = self._model._orig_forward

0 commit comments

Comments
 (0)