From 9f38c8b43dc4bc17f90a0146f074273559154b91 Mon Sep 17 00:00:00 2001 From: HaD0Yun Date: Sun, 22 Mar 2026 19:16:14 +0900 Subject: [PATCH] Clarify README usage of InferenceOptions for generation Issue #15 is a docs/API alignment fix: the README should show that advanced generation controls are configured through the InferenceOptions dataclass and passed via the inference_options argument, while a small AST-based regression test protects that README-facing API surface from drifting out of sync. Constraint: Keep the change scoped to the existing issue #15 docs/API guidance gap Rejected: Runtime API aliases for top-level kwargs | unnecessary behavior change for a documentation issue Rejected: README-only fix without regression coverage | easy for docs and API surface to drift apart Confidence: high Scope-risk: narrow Directive: Keep README inference examples aligned with tada/modules/tada.py and update tests/tada_api_surface_test.py when the generate signature or InferenceOptions defaults change Tested: git diff --check; uv run --python 3.12 pytest tests/tada_api_surface_test.py -q; static README/API compatibility check Not-tested: End-to-end model inference with downloaded weights/audio assets --- README.md | 37 ++++++++++++++- tests/tada_api_surface_test.py | 83 ++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 tests/tada_api_surface_test.py diff --git a/README.md b/README.md index 6125a7a..a20a0f8 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ import torch import torchaudio from tada.modules.encoder import Encoder, EncoderOutput -from tada.modules.tada import TadaForCausalLM +from tada.modules.tada import InferenceOptions, TadaForCausalLM device = "cuda" @@ -133,6 +133,41 @@ output = model.generate( ) ``` +### Custom inference settings + +Advanced acoustic parameters such as `noise_temperature`, `acoustic_cfg_scale`, `duration_cfg_scale`, +`num_flow_matching_steps`, and other sampling controls are configured through the `InferenceOptions` +dataclass. Pass them via the `inference_options` argument—`model.generate()` does not accept these values as +top-level keyword arguments. + +```python +opts = InferenceOptions( + acoustic_cfg_scale=1.8, + duration_cfg_scale=1.0, + num_flow_matching_steps=20, + num_acoustic_candidates=4, + scorer="spkr_verification", + spkr_verification_weight=1.0, + negative_condition_source="prompt", + speed_up_factor=1.1, +) + +output = model.generate( + prompt=prompt, + text="Please call Stella. Ask her to bring these things with her from the store.", + inference_options=opts, +) +``` + +Useful options: + +- `acoustic_cfg_scale`, `duration_cfg_scale`, `cfg_schedule` — control classifier-free guidance strength and schedule. +- `num_flow_matching_steps`, `noise_temperature`, `time_schedule` — trade off generation speed vs. acoustic quality. +- `text_temperature`, `text_top_p`, `text_top_k`, `text_repetition_penalty` — control text-token sampling behavior. +- `negative_condition_source` — choose how the negative branch is built (`"negative_step_output"`, `"prompt"`, or `"zero"`). +- `num_acoustic_candidates`, `scorer`, `spkr_verification_weight` — generate multiple acoustic candidates and rank them by likelihood, duration median, or speaker verification. +- `speed_up_factor` — shorten predicted durations for faster speech. + ### Multilingual Generation TADA supports multilingual speech synthesis via language-specific aligners. Pass the `language` parameter when loading the encoder to use the appropriate aligner for your target language. diff --git a/tests/tada_api_surface_test.py b/tests/tada_api_surface_test.py new file mode 100644 index 0000000..d0d2ed7 --- /dev/null +++ b/tests/tada_api_surface_test.py @@ -0,0 +1,83 @@ +import ast +from pathlib import Path + + +SOURCE_PATH = Path(__file__).resolve().parents[1] / "tada/modules/tada.py" + + +EXPECTED_INFERENCE_OPTION_DEFAULTS = { + "text_do_sample": True, + "text_temperature": 0.6, + "text_top_k": 0, + "text_top_p": 0.9, + "text_repetition_penalty": 1.1, + "acoustic_cfg_scale": 1.6, + "duration_cfg_scale": 1.0, + "cfg_schedule": "cosine", + "noise_temperature": 0.9, + "num_flow_matching_steps": 10, + "time_schedule": "logsnr", + "num_acoustic_candidates": 1, + "scorer": "likelihood", + "spkr_verification_weight": 1.0, + "speed_up_factor": None, + "negative_condition_source": "negative_step_output", + "text_only_logit_scale": 0.0, +} + +EXPECTED_GENERATE_PARAMS = [ + "self", + "prompt", + "text", + "num_transition_steps", + "num_extra_steps", + "system_prompt", + "user_turn_prompt", + "inference_options", + "use_text_in_prompt", + "normalize_text", + "verbose", +] + + +MODULE = ast.parse(SOURCE_PATH.read_text()) + + +def _find_class(name: str) -> ast.ClassDef: + for node in MODULE.body: + if isinstance(node, ast.ClassDef) and node.name == name: + return node + raise AssertionError(f"Class {name} not found in {SOURCE_PATH}") + + +def _literal_value(node: ast.expr): + if isinstance(node, ast.Constant): + return node.value + raise AssertionError(f"Unsupported default value AST: {ast.dump(node)}") + + +def test_inference_options_defaults_match_readme_facing_api_surface(): + inference_options = _find_class("InferenceOptions") + + assert any(isinstance(decorator, ast.Name) and decorator.id == "dataclass" for decorator in inference_options.decorator_list) + + actual_defaults = {} + for node in inference_options.body: + if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.value is not None: + actual_defaults[node.target.id] = _literal_value(node.value) + + assert actual_defaults == EXPECTED_INFERENCE_OPTION_DEFAULTS + + +def test_generate_accepts_inference_options_parameter_with_dataclass_default(): + tada_for_causal_lm = _find_class("TadaForCausalLM") + + generate = next( + node for node in tada_for_causal_lm.body if isinstance(node, ast.FunctionDef) and node.name == "generate" + ) + + actual_params = [arg.arg for arg in generate.args.args] + assert actual_params == EXPECTED_GENERATE_PARAMS + + defaults_by_name = dict(zip(actual_params[-len(generate.args.defaults) :], generate.args.defaults)) + assert ast.unparse(defaults_by_name["inference_options"]) == "InferenceOptions()"