ajh-code commited on
Commit
c41750d
·
verified ·
1 Parent(s): 2c8c506

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. runtime-data/references/.gitkeep +1 -0
  2. runtime/experimental/__init__.py +1 -0
  3. runtime/experimental/codec.py +336 -0
  4. runtime/native/LICENSE +202 -0
  5. runtime/native/direct_w4a4_m1/direct_w4a4_m1.cpp +104 -0
  6. runtime/native/rmsnorm_nvfp4_m1/rmsnorm_nvfp4_m1.cpp +38 -0
  7. runtime/native/rmsnorm_nvfp4_m1/rmsnorm_nvfp4_m1.cu +160 -0
  8. runtime/native/rmsnorm_nvfp4_m1/rmsnorm_nvfp4_m1.h +10 -0
  9. runtime/native/smallm_gemv/smallm_gemv.cpp +86 -0
  10. runtime/native/smallm_gemv/smallm_gemv.cu +171 -0
  11. runtime/native/smallm_gemv/smallm_gemv.h +12 -0
  12. runtime/web/index.html +234 -0
  13. vendor/fish-speech/.github/ISSUE_TEMPLATE/bug_report.yml +63 -0
  14. vendor/fish-speech/.github/ISSUE_TEMPLATE/config.yml +5 -0
  15. vendor/fish-speech/.github/ISSUE_TEMPLATE/feature_request.yml +64 -0
  16. vendor/fish-speech/.github/workflows/build-docker-image.yml +78 -0
  17. vendor/fish-speech/.github/workflows/docs.yml +33 -0
  18. vendor/fish-speech/.github/workflows/stale.yml +25 -0
  19. vendor/fish-speech/awesome_webui/public/vite.svg +1 -0
  20. vendor/fish-speech/awesome_webui/src/App.tsx +1185 -0
  21. vendor/fish-speech/awesome_webui/src/assets/react.svg +1 -0
  22. vendor/fish-speech/awesome_webui/src/components/ui/alert.tsx +35 -0
  23. vendor/fish-speech/awesome_webui/src/components/ui/badge.tsx +31 -0
  24. vendor/fish-speech/awesome_webui/src/components/ui/button.tsx +44 -0
  25. vendor/fish-speech/awesome_webui/src/components/ui/card.tsx +31 -0
  26. vendor/fish-speech/awesome_webui/src/components/ui/collapsible.tsx +7 -0
  27. vendor/fish-speech/awesome_webui/src/components/ui/dialog.tsx +86 -0
  28. vendor/fish-speech/awesome_webui/src/components/ui/label.tsx +15 -0
  29. vendor/fish-speech/awesome_webui/src/components/ui/scroll-area.tsx +43 -0
  30. vendor/fish-speech/awesome_webui/src/components/ui/separator.tsx +26 -0
  31. vendor/fish-speech/awesome_webui/src/components/ui/slider.tsx +23 -0
  32. vendor/fish-speech/awesome_webui/src/components/ui/switch.tsx +27 -0
  33. vendor/fish-speech/awesome_webui/src/components/ui/textarea.tsx +17 -0
  34. vendor/fish-speech/awesome_webui/src/components/ui/toggle-group.tsx +49 -0
  35. vendor/fish-speech/awesome_webui/src/index.css +71 -0
  36. vendor/fish-speech/awesome_webui/src/lib/utils.ts +6 -0
  37. vendor/fish-speech/awesome_webui/src/main.tsx +10 -0
  38. vendor/fish-speech/docs/ar/finetune.md +125 -0
  39. vendor/fish-speech/docs/ar/index.md +192 -0
  40. vendor/fish-speech/docs/ar/inference.md +98 -0
  41. vendor/fish-speech/docs/ar/install.md +189 -0
  42. vendor/fish-speech/docs/assets/logo.svg +29 -0
  43. vendor/fish-speech/docs/en/finetune.md +128 -0
  44. vendor/fish-speech/docs/en/index.md +191 -0
  45. vendor/fish-speech/docs/en/inference.md +98 -0
  46. vendor/fish-speech/docs/en/install.md +228 -0
  47. vendor/fish-speech/docs/en/server.md +80 -0
  48. vendor/fish-speech/docs/es/finetune.md +131 -0
  49. vendor/fish-speech/docs/es/index.md +191 -0
  50. vendor/fish-speech/docs/es/inference.md +58 -0
runtime-data/references/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
runtime/experimental/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Experimental code kept separate from the pinned Fish Speech baseline."""
runtime/experimental/codec.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Project-local inference memory optimizations for the S2-Pro DAC codec.
2
+
3
+ The pinned Fish Speech source remains unchanged. This module builds the same
4
+ codec and loads the same checkpoint, then removes buffers that are unnecessary
5
+ for the window-limited inference path before the model is moved to CUDA.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import gc
11
+ import io
12
+ import math
13
+ import os
14
+ import threading
15
+ import time
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ import torch
20
+
21
+ os.environ.setdefault("TEMPORARILY_DISABLE_PROTOBUF_VERSION_CHECK", "true")
22
+
23
+ from fish_speech.models.dac.modded_dac import DAC
24
+
25
+
26
+ def load_reference_audio_soundfile(
27
+ reference_audio: bytes | str | Path,
28
+ sample_rate: int,
29
+ ):
30
+ """Decode API reference audio without TorchCodec.
31
+
32
+ Torch 2.11 routes ``torchaudio.load`` through optional TorchCodec. The
33
+ pinned container already includes SoundFile, which supports the WAV/FLAC
34
+ inputs accepted by this service, so no environment package mutation is
35
+ needed.
36
+ """
37
+
38
+ import numpy as np
39
+ import soundfile as sf
40
+ import torchaudio
41
+
42
+ source = (
43
+ io.BytesIO(reference_audio)
44
+ if isinstance(reference_audio, bytes)
45
+ else reference_audio
46
+ )
47
+ audio, original_rate = sf.read(source, dtype="float32", always_2d=True)
48
+ mono = np.asarray(audio.mean(axis=1), dtype=np.float32)
49
+ if original_rate != sample_rate:
50
+ mono = (
51
+ torchaudio.functional.resample(
52
+ torch.from_numpy(mono),
53
+ original_rate,
54
+ sample_rate,
55
+ )
56
+ .contiguous()
57
+ .numpy()
58
+ )
59
+ return mono
60
+
61
+
62
+ @torch.inference_mode()
63
+ def warm_reference_encoder(
64
+ codec: torch.nn.Module,
65
+ device: str | torch.device,
66
+ seconds: float = 1.0,
67
+ ) -> dict[str, Any]:
68
+ """Prime lazy codec state before the first user reference is cached.
69
+
70
+ The staged BF16 encoder produces a different discrete code sequence on its
71
+ first CUDA pass. A discarded silence pass makes subsequent encodes bit
72
+ stable, preventing the first uploaded voice from being conditioned on an
73
+ avoidable cold-start code path.
74
+ """
75
+
76
+ if seconds <= 0:
77
+ raise ValueError("Reference warmup duration must be positive")
78
+ target = torch.device(device)
79
+ sample_rate = int(codec.sample_rate)
80
+ samples = int(round(sample_rate * seconds))
81
+ audio = torch.zeros((1, samples), dtype=torch.float32)
82
+ lengths = torch.tensor([samples], device=target, dtype=torch.long)
83
+ started = time.perf_counter()
84
+ codes = codec.encode(audio, lengths)[0][0].cpu()
85
+ return {
86
+ "input": "digital_silence",
87
+ "seconds": seconds,
88
+ "samples": samples,
89
+ "code_frames": int(codes.shape[-1]),
90
+ "elapsed_seconds": time.perf_counter() - started,
91
+ }
92
+
93
+
94
+ class StagedReferenceCodec(DAC):
95
+ """DAC with decode modules resident and reference-only modules staged."""
96
+
97
+ @property
98
+ def device(self) -> torch.device:
99
+ return self._decode_device
100
+
101
+ def configure_reference_staging(
102
+ self,
103
+ decode_device: str | torch.device,
104
+ offload_device: str | torch.device = "cpu",
105
+ ) -> None:
106
+ self._decode_device = torch.device(decode_device)
107
+ self._reference_offload_device = torch.device(offload_device)
108
+ self._reference_lock = threading.Lock()
109
+
110
+ # These modules are used by ``from_indices`` and remain resident.
111
+ self.quantizer.semantic_quantizer.to(self._decode_device)
112
+ self.quantizer.quantizer.to(self._decode_device)
113
+ self.quantizer.post_module.to(self._decode_device)
114
+ self.quantizer.upsample.to(self._decode_device)
115
+ self.decoder.to(self._decode_device)
116
+
117
+ # These modules are required only while a new reference is encoded.
118
+ self.encoder.to(self._reference_offload_device)
119
+ self.quantizer.downsample.to(self._reference_offload_device)
120
+ self.quantizer.pre_module.to(self._reference_offload_device)
121
+
122
+ @torch.inference_mode()
123
+ def encode(
124
+ self,
125
+ audio_data: torch.Tensor,
126
+ audio_lengths: torch.Tensor | None = None,
127
+ n_quantizers: int | None = None,
128
+ **kwargs,
129
+ ):
130
+ """Encode reference codes, staging only the required modules on CUDA."""
131
+
132
+ if not hasattr(self, "_reference_lock"):
133
+ return super().encode(
134
+ audio_data,
135
+ audio_lengths=audio_lengths,
136
+ n_quantizers=n_quantizers,
137
+ **kwargs,
138
+ )
139
+
140
+ with self._reference_lock:
141
+ reference_modules = (
142
+ self.encoder,
143
+ self.quantizer.downsample,
144
+ self.quantizer.pre_module,
145
+ )
146
+ for module in reference_modules:
147
+ module.to(self._decode_device)
148
+
149
+ try:
150
+ dtype = next(self.encoder.parameters()).dtype
151
+ audio_data = audio_data.to(device=self._decode_device, dtype=dtype)
152
+ if audio_data.ndim == 2:
153
+ audio_data = audio_data.unsqueeze(1)
154
+ length = audio_data.shape[-1]
155
+ right_pad = (
156
+ math.ceil(length / self.frame_length) * self.frame_length - length
157
+ )
158
+ audio_data = torch.nn.functional.pad(audio_data, (0, right_pad))
159
+ if audio_lengths is None:
160
+ audio_lengths = torch.tensor(
161
+ [length + right_pad],
162
+ device=self._decode_device,
163
+ dtype=torch.long,
164
+ )
165
+ else:
166
+ audio_lengths = audio_lengths.to(self._decode_device)
167
+
168
+ z = self.encoder(audio_data)
169
+ z = self.quantizer.downsample(z)
170
+ z = self.quantizer.pre_module(z)
171
+ semantic_z, semantic_codes, *_ = self.quantizer.semantic_quantizer(z)
172
+ residual_z = z - semantic_z
173
+ _, residual_codes, *_ = self.quantizer.quantizer(
174
+ residual_z,
175
+ n_quantizers=n_quantizers,
176
+ )
177
+ indices = torch.cat([semantic_codes, residual_codes], dim=1)
178
+ indices_lens = torch.ceil(audio_lengths / self.frame_length).long()
179
+ finally:
180
+ if self._decode_device.type == "cuda":
181
+ torch.cuda.synchronize(self._decode_device)
182
+ for module in reference_modules:
183
+ module.to(self._reference_offload_device)
184
+ if self._decode_device.type == "cuda":
185
+ torch.cuda.empty_cache()
186
+
187
+ return indices, indices_lens
188
+
189
+
190
+ def _tensor_bytes(tensor: torch.Tensor | None) -> int:
191
+ if tensor is None:
192
+ return 0
193
+ return tensor.numel() * tensor.element_size()
194
+
195
+
196
+ @torch.inference_mode()
197
+ def compact_codec_inference_buffers(codec: torch.nn.Module) -> dict[str, Any]:
198
+ """Remove dead causal masks and bound RoPE tables to configured limits.
199
+
200
+ ``WindowLimitedTransformer.forward`` always constructs an exact mask for
201
+ the current input and passes it to its parent implementation. Therefore the
202
+ inherited 32768-square causal mask is not read on this path. Its RoPE table
203
+ is used, but the configured block size is the model's supported inference
204
+ limit and is far smaller than the inherited 327680-frame table.
205
+ """
206
+
207
+ from fish_speech.models.dac.modded_dac import WindowLimitedTransformer
208
+
209
+ records: list[dict[str, Any]] = []
210
+ saved_bytes = 0
211
+ for name, module in codec.named_modules():
212
+ if not isinstance(module, WindowLimitedTransformer):
213
+ continue
214
+
215
+ causal_mask = module.causal_mask
216
+ freqs_cis = module.freqs_cis
217
+ if freqs_cis is None:
218
+ raise RuntimeError(f"Codec transformer {name} has no RoPE table")
219
+
220
+ frame_limit = int(module.config.block_size)
221
+ if frame_limit <= 0 or frame_limit > freqs_cis.shape[0]:
222
+ raise RuntimeError(
223
+ f"Invalid codec RoPE limit for {name}: {frame_limit} "
224
+ f"of {freqs_cis.shape[0]}"
225
+ )
226
+
227
+ before_mask_bytes = _tensor_bytes(causal_mask)
228
+ before_rope_bytes = _tensor_bytes(freqs_cis)
229
+ device = freqs_cis.device
230
+ module.causal_mask = torch.empty(0, dtype=torch.bool, device=device)
231
+ module.freqs_cis = freqs_cis[:frame_limit].clone()
232
+ after_rope_bytes = _tensor_bytes(module.freqs_cis)
233
+ module._compact_inference_frame_limit = frame_limit
234
+
235
+ records.append(
236
+ {
237
+ "module": name,
238
+ "frame_limit": frame_limit,
239
+ "removed_causal_mask_bytes": before_mask_bytes,
240
+ "rope_bytes_before": before_rope_bytes,
241
+ "rope_bytes_after": after_rope_bytes,
242
+ }
243
+ )
244
+ saved_bytes += before_mask_bytes + before_rope_bytes - after_rope_bytes
245
+
246
+ if len(records) != 3:
247
+ raise RuntimeError(
248
+ f"Expected three window-limited codec transformers, found {len(records)}"
249
+ )
250
+
251
+ report = {
252
+ "policy": "compact_windowed_inference_buffers",
253
+ "windowed_transformers": len(records),
254
+ "theoretical_saved_bytes": saved_bytes,
255
+ "records": records,
256
+ }
257
+ codec._compact_inference_buffers_report = report
258
+ return report
259
+
260
+
261
+ @torch.inference_mode()
262
+ def load_compact_codec_model(
263
+ config_name: str,
264
+ checkpoint_path: str | Path,
265
+ device: str | torch.device = "cuda:0",
266
+ precision: torch.dtype = torch.bfloat16,
267
+ offload_reference: bool = False,
268
+ ) -> torch.nn.Module:
269
+ """Load the pinned codec with compact buffers before CUDA placement."""
270
+
271
+ from hydra.utils import instantiate
272
+ from omegaconf import OmegaConf
273
+
274
+ from fish_speech.models.dac import modded_dac as modded_dac_module
275
+
276
+ config_path = (
277
+ Path(modded_dac_module.__file__).resolve().parents[2]
278
+ / "configs"
279
+ / f"{config_name}.yaml"
280
+ )
281
+ cfg = OmegaConf.load(config_path)
282
+ if offload_reference:
283
+ cfg._target_ = "experimental.codec.StagedReferenceCodec"
284
+
285
+ codec = instantiate(cfg)
286
+ state_dict = torch.load(
287
+ checkpoint_path,
288
+ map_location="cpu",
289
+ mmap=True,
290
+ weights_only=True,
291
+ )
292
+ if "state_dict" in state_dict:
293
+ state_dict = state_dict["state_dict"]
294
+ if any("generator" in key for key in state_dict):
295
+ state_dict = {
296
+ key.replace("generator.", ""): value
297
+ for key, value in state_dict.items()
298
+ if "generator." in key
299
+ }
300
+
301
+ load_result = codec.load_state_dict(state_dict, strict=False, assign=True)
302
+ unexpected = [
303
+ key
304
+ for key in load_result.unexpected_keys
305
+ if not key.endswith(("causal_mask", "freqs_cis"))
306
+ ]
307
+ if load_result.missing_keys or unexpected:
308
+ raise RuntimeError(
309
+ "Unexpected compact codec checkpoint mismatch: "
310
+ f"missing={load_result.missing_keys[:5]}, unexpected={unexpected[:5]}"
311
+ )
312
+
313
+ report = compact_codec_inference_buffers(codec)
314
+ codec.eval()
315
+ codec.to(dtype=precision)
316
+ if offload_reference:
317
+ codec.configure_reference_staging(device)
318
+ report["reference_path"] = "staged_from_cpu_to_decode_device"
319
+ else:
320
+ codec.to(device=device)
321
+ report["reference_path"] = "resident_on_decode_device"
322
+ codec._compact_inference_buffers_report = report
323
+ del state_dict
324
+ gc.collect()
325
+ if torch.cuda.is_available() and torch.device(device).type == "cuda":
326
+ torch.cuda.empty_cache()
327
+ return codec
328
+
329
+
330
+ __all__ = [
331
+ "StagedReferenceCodec",
332
+ "compact_codec_inference_buffers",
333
+ "load_compact_codec_model",
334
+ "load_reference_audio_soundfile",
335
+ "warm_reference_encoder",
336
+ ]
runtime/native/LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Support. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright (c) 2025 Comfy Org. All rights reserved.
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
runtime/native/direct_w4a4_m1/direct_w4a4_m1.cpp ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "direct_w4a4_m1.h"
2
+
3
+ #include <torch/extension.h>
4
+
5
+ #include <optional>
6
+
7
+ namespace {
8
+
9
+ torch::Tensor direct_w4a4_m1_linear(
10
+ const torch::Tensor& activation_qdata,
11
+ const torch::Tensor& activation_block_scales,
12
+ const torch::Tensor& activation_tensor_scale,
13
+ const torch::Tensor& weight_qdata,
14
+ const torch::Tensor& weight_block_scales,
15
+ const torch::Tensor& weight_tensor_scale,
16
+ const std::optional<torch::Tensor>& bias) {
17
+ TORCH_CHECK(
18
+ activation_qdata.is_cuda() &&
19
+ activation_qdata.scalar_type() == at::kByte &&
20
+ activation_qdata.dim() == 2 && activation_qdata.is_contiguous(),
21
+ "activation qdata must be contiguous CUDA uint8 [padded_M,K/2]");
22
+ TORCH_CHECK(
23
+ activation_qdata.size(0) >= 1,
24
+ "direct W4A4 M=1 requires at least one packed activation row");
25
+ TORCH_CHECK(
26
+ activation_block_scales.is_cuda() &&
27
+ activation_block_scales.dim() == 2 &&
28
+ activation_block_scales.is_contiguous() &&
29
+ activation_block_scales.element_size() == 1,
30
+ "activation block scales must be contiguous CUDA byte-sized [padded_M,padded_K/16]");
31
+ TORCH_CHECK(
32
+ activation_tensor_scale.is_cuda() &&
33
+ activation_tensor_scale.scalar_type() == at::kFloat &&
34
+ activation_tensor_scale.numel() == 1 &&
35
+ activation_tensor_scale.is_contiguous(),
36
+ "activation tensor scale must be one contiguous CUDA float32 value");
37
+ TORCH_CHECK(
38
+ weight_qdata.is_cuda() && weight_qdata.scalar_type() == at::kByte &&
39
+ weight_qdata.dim() == 2 && weight_qdata.is_contiguous(),
40
+ "weight qdata must be contiguous CUDA uint8 [N,K/2]");
41
+ TORCH_CHECK(
42
+ weight_block_scales.is_cuda() && weight_block_scales.dim() == 2 &&
43
+ weight_block_scales.is_contiguous() &&
44
+ weight_block_scales.element_size() == 1,
45
+ "weight block scales must be contiguous CUDA byte-sized [padded_N,padded_K/16]");
46
+ TORCH_CHECK(
47
+ weight_tensor_scale.is_cuda() &&
48
+ weight_tensor_scale.scalar_type() == at::kFloat &&
49
+ weight_tensor_scale.numel() == 1 &&
50
+ weight_tensor_scale.is_contiguous(),
51
+ "weight tensor scale must be one contiguous CUDA float32 value");
52
+ TORCH_CHECK(
53
+ activation_qdata.device() == activation_block_scales.device() &&
54
+ activation_qdata.device() == activation_tensor_scale.device() &&
55
+ activation_qdata.device() == weight_qdata.device() &&
56
+ activation_qdata.device() == weight_block_scales.device() &&
57
+ activation_qdata.device() == weight_tensor_scale.device(),
58
+ "all direct W4A4 tensors must use the same CUDA device");
59
+
60
+ const int64_t in_features = activation_qdata.size(1) * 2;
61
+ const int64_t out_features = weight_qdata.size(0);
62
+ TORCH_CHECK(
63
+ in_features > 0 && in_features % 32 == 0,
64
+ "direct W4A4 M=1 requires K divisible by 32");
65
+ TORCH_CHECK(
66
+ weight_qdata.size(1) == activation_qdata.size(1),
67
+ "activation and weight packed K dimensions differ");
68
+ TORCH_CHECK(out_features > 0, "direct W4A4 M=1 requires positive N");
69
+ TORCH_CHECK(
70
+ activation_block_scales.size(0) >= 1 &&
71
+ activation_block_scales.size(1) >= in_features / 16,
72
+ "activation block-scale tensor is too small");
73
+ TORCH_CHECK(
74
+ weight_block_scales.size(0) >= out_features &&
75
+ weight_block_scales.size(1) >= in_features / 16,
76
+ "weight block-scale tensor is too small");
77
+
78
+ if (bias.has_value()) {
79
+ const auto& value = *bias;
80
+ TORCH_CHECK(
81
+ value.is_cuda() && value.scalar_type() == at::kBFloat16 &&
82
+ value.dim() == 1 && value.is_contiguous() &&
83
+ value.numel() == out_features &&
84
+ value.device() == activation_qdata.device(),
85
+ "bias must be contiguous CUDA bfloat16 [N]");
86
+ }
87
+ return direct_w4a4_m1_linear_cuda(
88
+ activation_qdata,
89
+ activation_block_scales,
90
+ activation_tensor_scale,
91
+ weight_qdata,
92
+ weight_block_scales,
93
+ weight_tensor_scale,
94
+ bias);
95
+ }
96
+
97
+ } // namespace
98
+
99
+ PYBIND11_MODULE(TORCH_EXTENSION_NAME, module) {
100
+ module.def(
101
+ "linear",
102
+ &direct_w4a4_m1_linear,
103
+ "Direct packed-NVFP4 activation x packed-NVFP4 weight M=1 linear");
104
+ }
runtime/native/rmsnorm_nvfp4_m1/rmsnorm_nvfp4_m1.cpp ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "rmsnorm_nvfp4_m1.h"
2
+
3
+ #include <torch/extension.h>
4
+
5
+ #include <vector>
6
+
7
+ namespace {
8
+
9
+ std::vector<torch::Tensor> rmsnorm_nvfp4_m1(
10
+ const torch::Tensor& input,
11
+ const torch::Tensor& weight,
12
+ double epsilon) {
13
+ TORCH_CHECK(
14
+ input.is_cuda() && input.scalar_type() == at::kBFloat16 &&
15
+ input.dim() == 2 && input.size(0) == 1 && input.is_contiguous(),
16
+ "RMSNorm input must be contiguous CUDA bfloat16 [1,K]");
17
+ TORCH_CHECK(
18
+ weight.is_cuda() && weight.scalar_type() == at::kBFloat16 &&
19
+ weight.dim() == 1 && weight.is_contiguous(),
20
+ "RMSNorm weight must be contiguous CUDA bfloat16 [K]");
21
+ TORCH_CHECK(
22
+ weight.device() == input.device() && weight.numel() == input.size(1),
23
+ "RMSNorm input and weight dimensions/devices differ");
24
+ TORCH_CHECK(
25
+ input.size(1) > 0 && input.size(1) % 32 == 0,
26
+ "fused RMSNorm-to-NVFP4 requires K divisible by 32");
27
+ TORCH_CHECK(epsilon > 0.0, "RMSNorm epsilon must be positive");
28
+ return rmsnorm_nvfp4_m1_cuda(input, weight, epsilon);
29
+ }
30
+
31
+ } // namespace
32
+
33
+ PYBIND11_MODULE(TORCH_EXTENSION_NAME, module) {
34
+ module.def(
35
+ "quantize",
36
+ &rmsnorm_nvfp4_m1,
37
+ "Fused Qwen RMSNorm to packed NVFP4 at logical M=1");
38
+ }
runtime/native/rmsnorm_nvfp4_m1/rmsnorm_nvfp4_m1.cu ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "rmsnorm_nvfp4_m1.h"
2
+
3
+ #include <ATen/cuda/CUDAContext.h>
4
+ #include <c10/cuda/CUDAGuard.h>
5
+ #include <cuda_bf16.h>
6
+ #include <cuda_fp4.h>
7
+ #include <cuda_fp8.h>
8
+ #include <cuda_runtime.h>
9
+
10
+ #include <cstdint>
11
+ #include <vector>
12
+
13
+ namespace {
14
+
15
+ constexpr int kThreads = 256;
16
+ constexpr int kFp4BlockElements = 16;
17
+ constexpr int kScaleTileOuter = 128;
18
+ constexpr float kFp4Max = 6.0f;
19
+ constexpr float kFp8Max = 448.0f;
20
+ constexpr float kCombinedMax = kFp4Max * kFp8Max;
21
+
22
+ __device__ __forceinline__ size_t scale_offset(
23
+ int outer,
24
+ int inner_scale,
25
+ int scale_inner_dim) {
26
+ const int outer_tile = outer / kScaleTileOuter;
27
+ const int local_outer = outer % kScaleTileOuter;
28
+ const int local_inner = inner_scale & 3;
29
+ const int inner_tile_start = inner_scale - local_inner;
30
+ const size_t tile_base =
31
+ static_cast<size_t>(inner_tile_start + outer_tile * scale_inner_dim) *
32
+ kScaleTileOuter;
33
+ return tile_base + static_cast<size_t>(local_outer & 31) * 16 +
34
+ static_cast<size_t>(local_outer >> 5) * 4 + local_inner;
35
+ }
36
+
37
+ __device__ __forceinline__ float e4m3_to_float(uint8_t raw) {
38
+ const __half_raw half_raw = __nv_cvt_fp8_to_halfraw(raw, __NV_E4M3);
39
+ return __half2float(static_cast<__half>(half_raw));
40
+ }
41
+
42
+ __device__ __forceinline__ float qwen_rmsnorm_value(
43
+ const __nv_bfloat16* input,
44
+ const __nv_bfloat16* weight,
45
+ int index,
46
+ float inverse_rms) {
47
+ const float normalized = __bfloat162float(input[index]) * inverse_rms;
48
+ const __nv_bfloat16 normalized_bf16 = __float2bfloat16_rn(normalized);
49
+ return __bfloat162float(__float2bfloat16_rn(
50
+ __bfloat162float(normalized_bf16) * __bfloat162float(weight[index])));
51
+ }
52
+
53
+ __global__ void rmsnorm_nvfp4_m1_kernel(
54
+ const __nv_bfloat16* __restrict__ input,
55
+ const __nv_bfloat16* __restrict__ weight,
56
+ uint8_t* __restrict__ qdata,
57
+ uint8_t* __restrict__ block_scales,
58
+ float* __restrict__ tensor_scale,
59
+ int k,
60
+ int scale_inner_dim,
61
+ float epsilon) {
62
+ __shared__ float reduction[kThreads];
63
+ const int tid = threadIdx.x;
64
+ float sum_square = 0.0f;
65
+ for (int index = tid; index < k; index += kThreads) {
66
+ const float value = __bfloat162float(input[index]);
67
+ sum_square = fmaf(value, value, sum_square);
68
+ }
69
+ reduction[tid] = sum_square;
70
+ __syncthreads();
71
+ for (int offset = kThreads / 2; offset > 0; offset >>= 1) {
72
+ if (tid < offset) {
73
+ reduction[tid] += reduction[tid + offset];
74
+ }
75
+ __syncthreads();
76
+ }
77
+ const float inverse_rms = rsqrtf(reduction[0] / static_cast<float>(k) + epsilon);
78
+
79
+ float local_max = 0.0f;
80
+ for (int index = tid; index < k; index += kThreads) {
81
+ local_max = fmaxf(
82
+ local_max,
83
+ fabsf(qwen_rmsnorm_value(input, weight, index, inverse_rms)));
84
+ }
85
+ reduction[tid] = local_max;
86
+ __syncthreads();
87
+ for (int offset = kThreads / 2; offset > 0; offset >>= 1) {
88
+ if (tid < offset) {
89
+ reduction[tid] = fmaxf(reduction[tid], reduction[tid + offset]);
90
+ }
91
+ __syncthreads();
92
+ }
93
+ // TensorCoreNVFP4Layout computes the default scale from a BF16 amax, so
94
+ // the division result is rounded to BF16 before Params converts it to F32.
95
+ const float global_scale = __bfloat162float(__float2bfloat16_rn(
96
+ __fdiv_rn(reduction[0], kCombinedMax)));
97
+ if (tid == 0) {
98
+ tensor_scale[0] = global_scale;
99
+ }
100
+ __syncthreads();
101
+
102
+ const int block_count = k / kFp4BlockElements;
103
+ for (int block = tid; block < block_count; block += kThreads) {
104
+ const int base = block * kFp4BlockElements;
105
+ float values[kFp4BlockElements];
106
+ float block_max = 0.0f;
107
+ #pragma unroll
108
+ for (int element = 0; element < kFp4BlockElements; ++element) {
109
+ values[element] = qwen_rmsnorm_value(
110
+ input, weight, base + element, inverse_rms);
111
+ block_max = fmaxf(block_max, fabsf(values[element]));
112
+ }
113
+ float scaled_block_scale = (block_max / kFp4Max) / global_scale;
114
+ scaled_block_scale = fminf(scaled_block_scale, kFp8Max);
115
+ const uint8_t raw_block_scale = static_cast<uint8_t>(
116
+ __nv_cvt_float_to_fp8(
117
+ scaled_block_scale, __NV_SATFINITE, __NV_E4M3));
118
+ block_scales[scale_offset(0, block, scale_inner_dim)] = raw_block_scale;
119
+ const float total_scale = global_scale * e4m3_to_float(raw_block_scale);
120
+ #pragma unroll
121
+ for (int pair = 0; pair < kFp4BlockElements / 2; ++pair) {
122
+ const float even = values[pair * 2] / total_scale;
123
+ const float odd = values[pair * 2 + 1] / total_scale;
124
+ const float2 arguments = make_float2(odd, even);
125
+ qdata[base / 2 + pair] = static_cast<uint8_t>(
126
+ __nv_cvt_float2_to_fp4x2(arguments, __NV_E2M1, cudaRoundNearest));
127
+ }
128
+ }
129
+ }
130
+
131
+ } // namespace
132
+
133
+ std::vector<torch::Tensor> rmsnorm_nvfp4_m1_cuda(
134
+ const torch::Tensor& input,
135
+ const torch::Tensor& weight,
136
+ double epsilon) {
137
+ const auto device = input.device();
138
+ c10::cuda::CUDAGuard guard(device);
139
+ const int64_t k = input.size(1);
140
+ const int64_t scale_cols = (k / 16 + 3) / 4 * 4;
141
+ torch::Tensor qdata = torch::zeros(
142
+ {16, k / 2}, input.options().dtype(at::kByte));
143
+ torch::Tensor block_scales = torch::zeros(
144
+ {128, scale_cols},
145
+ input.options().dtype(at::ScalarType::Float8_e4m3fn));
146
+ torch::Tensor tensor_scale = torch::empty(
147
+ {}, input.options().dtype(at::kFloat));
148
+ const auto stream = at::cuda::getCurrentCUDAStream(device.index()).stream();
149
+ rmsnorm_nvfp4_m1_kernel<<<1, kThreads, 0, stream>>>(
150
+ reinterpret_cast<const __nv_bfloat16*>(input.data_ptr()),
151
+ reinterpret_cast<const __nv_bfloat16*>(weight.data_ptr()),
152
+ reinterpret_cast<uint8_t*>(qdata.data_ptr()),
153
+ reinterpret_cast<uint8_t*>(block_scales.data_ptr()),
154
+ reinterpret_cast<float*>(tensor_scale.data_ptr()),
155
+ static_cast<int>(k),
156
+ static_cast<int>(scale_cols),
157
+ static_cast<float>(epsilon));
158
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
159
+ return {qdata, block_scales, tensor_scale};
160
+ }
runtime/native/rmsnorm_nvfp4_m1/rmsnorm_nvfp4_m1.h ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+
3
+ #include <torch/extension.h>
4
+
5
+ #include <vector>
6
+
7
+ std::vector<torch::Tensor> rmsnorm_nvfp4_m1_cuda(
8
+ const torch::Tensor& input,
9
+ const torch::Tensor& weight,
10
+ double epsilon);
runtime/native/smallm_gemv/smallm_gemv.cpp ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "smallm_gemv.h"
2
+
3
+ #include <torch/extension.h>
4
+
5
+ #include <optional>
6
+
7
+ namespace {
8
+
9
+ torch::Tensor smallm_nvfp4_linear(
10
+ const torch::Tensor& input,
11
+ const torch::Tensor& packed_weight,
12
+ const torch::Tensor& weight_block_scales,
13
+ const torch::Tensor& weight_tensor_scale,
14
+ const std::optional<torch::Tensor>& bias) {
15
+ TORCH_CHECK(input.is_cuda(), "small-M GEMV requires a CUDA input");
16
+ TORCH_CHECK(
17
+ input.scalar_type() == at::kBFloat16,
18
+ "small-M GEMV input must be bfloat16");
19
+ TORCH_CHECK(
20
+ input.dim() >= 1 && input.is_contiguous(),
21
+ "small-M GEMV input must be contiguous");
22
+ TORCH_CHECK(
23
+ packed_weight.is_cuda() && packed_weight.scalar_type() == at::kByte &&
24
+ packed_weight.dim() == 2 && packed_weight.is_contiguous(),
25
+ "packed weight must be contiguous CUDA uint8 [N,K/2]");
26
+ TORCH_CHECK(
27
+ weight_block_scales.is_cuda() && weight_block_scales.dim() == 2 &&
28
+ weight_block_scales.is_contiguous() &&
29
+ weight_block_scales.element_size() == 1,
30
+ "weight block scales must be contiguous CUDA byte-sized [padded_N,padded_K/16]");
31
+ TORCH_CHECK(
32
+ weight_tensor_scale.is_cuda() &&
33
+ weight_tensor_scale.scalar_type() == at::kFloat &&
34
+ weight_tensor_scale.numel() == 1 &&
35
+ weight_tensor_scale.is_contiguous(),
36
+ "weight tensor scale must be one contiguous CUDA float32 value");
37
+ TORCH_CHECK(
38
+ input.device() == packed_weight.device() &&
39
+ input.device() == weight_block_scales.device() &&
40
+ input.device() == weight_tensor_scale.device(),
41
+ "all small-M GEMV tensors must use the same CUDA device");
42
+
43
+ const int64_t out_features = packed_weight.size(0);
44
+ const int64_t in_features = packed_weight.size(1) * 2;
45
+ TORCH_CHECK(
46
+ input.size(-1) == in_features,
47
+ "small-M GEMV expected input width ",
48
+ in_features,
49
+ " but got ",
50
+ input.size(-1));
51
+ TORCH_CHECK(
52
+ in_features > 0 && in_features % 32 == 0,
53
+ "small-M GEMV requires K divisible by 32");
54
+ TORCH_CHECK(
55
+ out_features > 0,
56
+ "small-M GEMV requires positive N");
57
+ TORCH_CHECK(
58
+ weight_block_scales.size(0) >= out_features &&
59
+ weight_block_scales.size(1) >= in_features / 16,
60
+ "weight block scale tensor is too small");
61
+
62
+ if (bias.has_value()) {
63
+ const auto& value = *bias;
64
+ TORCH_CHECK(
65
+ value.is_cuda() && value.scalar_type() == at::kBFloat16 &&
66
+ value.dim() == 1 && value.is_contiguous() &&
67
+ value.numel() == out_features &&
68
+ value.device() == input.device(),
69
+ "bias must be contiguous CUDA bfloat16 [N]");
70
+ }
71
+ return smallm_nvfp4_linear_cuda(
72
+ input,
73
+ packed_weight,
74
+ weight_block_scales,
75
+ weight_tensor_scale,
76
+ bias);
77
+ }
78
+
79
+ } // namespace
80
+
81
+ PYBIND11_MODULE(TORCH_EXTENSION_NAME, module) {
82
+ module.def(
83
+ "linear",
84
+ &smallm_nvfp4_linear,
85
+ "Fused BF16-activation x packed-NVFP4-weight small-M GEMV");
86
+ }
runtime/native/smallm_gemv/smallm_gemv.cu ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "smallm_gemv.h"
2
+
3
+ #include <ATen/cuda/CUDAContext.h>
4
+ #include <c10/cuda/CUDAGuard.h>
5
+ #include <cuda_bf16.h>
6
+ #include <cuda_fp8.h>
7
+ #include <cuda_runtime.h>
8
+
9
+ #include <cstdint>
10
+ #include <limits>
11
+ #include <optional>
12
+ #include <vector>
13
+
14
+ namespace {
15
+
16
+ constexpr int kWarpSize = 32;
17
+ constexpr int kWarpsPerBlock = 8;
18
+ constexpr int kThreads = kWarpSize * kWarpsPerBlock;
19
+ constexpr int kFp4BlockElements = 16;
20
+ constexpr int kScaleTileOuter = 128;
21
+
22
+ __device__ __forceinline__ size_t scale_offset(
23
+ int outer,
24
+ int inner_scale,
25
+ int scale_inner_dim) {
26
+ const int outer_tile = outer / kScaleTileOuter;
27
+ const int local_outer = outer % kScaleTileOuter;
28
+ const int local_inner = inner_scale & 3;
29
+ const int inner_tile_start = inner_scale - local_inner;
30
+ const size_t tile_base =
31
+ static_cast<size_t>(
32
+ inner_tile_start + outer_tile * scale_inner_dim) *
33
+ kScaleTileOuter;
34
+ return tile_base + static_cast<size_t>(local_outer & 31) * 16 +
35
+ static_cast<size_t>(local_outer >> 5) * 4 + local_inner;
36
+ }
37
+
38
+ __device__ __forceinline__ float e4m3_to_float(uint8_t raw) {
39
+ const __half_raw half_raw = __nv_cvt_fp8_to_halfraw(raw, __NV_E4M3);
40
+ return __half2float(static_cast<__half>(half_raw));
41
+ }
42
+
43
+ __device__ __forceinline__ float2 e2m1x2_to_float2(uint8_t packed) {
44
+ const __half2_raw raw =
45
+ __nv_cvt_fp4x2_to_halfraw2(packed, __NV_E2M1);
46
+ const __half2 converted(raw);
47
+ return __half22float2(converted);
48
+ }
49
+
50
+ __global__ void smallm_nvfp4_gemv_kernel(
51
+ const __nv_bfloat16* __restrict__ input,
52
+ const uint8_t* __restrict__ packed_weight,
53
+ const uint8_t* __restrict__ weight_block_scales,
54
+ const float* __restrict__ weight_tensor_scale,
55
+ const __nv_bfloat16* __restrict__ bias,
56
+ __nv_bfloat16* __restrict__ output,
57
+ int m,
58
+ int n,
59
+ int k,
60
+ int scale_inner_dim) {
61
+ const int lane = threadIdx.x & (kWarpSize - 1);
62
+ const int warp_in_block = threadIdx.x / kWarpSize;
63
+ const int64_t output_linear =
64
+ static_cast<int64_t>(blockIdx.x) * kWarpsPerBlock + warp_in_block;
65
+ const int64_t output_count = static_cast<int64_t>(m) * n;
66
+ if (output_linear >= output_count) {
67
+ return;
68
+ }
69
+
70
+ const int row_m = static_cast<int>(output_linear / n);
71
+ const int row_n = static_cast<int>(
72
+ output_linear - static_cast<int64_t>(row_m) * n);
73
+ const int packed_k = k / 2;
74
+ const __nv_bfloat16* input_row =
75
+ input + static_cast<int64_t>(row_m) * k;
76
+ const uint8_t* weight_row =
77
+ packed_weight + static_cast<int64_t>(row_n) * packed_k;
78
+ float accumulator = 0.0f;
79
+
80
+ for (int pair = lane; pair < packed_k; pair += kWarpSize) {
81
+ const uint8_t packed = weight_row[pair];
82
+ const int scale_block = pair / (kFp4BlockElements / 2);
83
+ const uint8_t scale_raw = weight_block_scales[
84
+ scale_offset(row_n, scale_block, scale_inner_dim)];
85
+ const float scale =
86
+ e4m3_to_float(scale_raw) * weight_tensor_scale[0];
87
+ const float2 weights = e2m1x2_to_float2(packed);
88
+ const int input_index = pair * 2;
89
+ accumulator = fmaf(
90
+ __bfloat162float(input_row[input_index]),
91
+ weights.y * scale,
92
+ accumulator);
93
+ accumulator = fmaf(
94
+ __bfloat162float(input_row[input_index + 1]),
95
+ weights.x * scale,
96
+ accumulator);
97
+ }
98
+
99
+ #pragma unroll
100
+ for (int offset = 16; offset > 0; offset >>= 1) {
101
+ accumulator += __shfl_down_sync(0xFFFFFFFF, accumulator, offset);
102
+ }
103
+ if (lane == 0) {
104
+ if (bias != nullptr) {
105
+ accumulator += __bfloat162float(bias[row_n]);
106
+ }
107
+ output[output_linear] = __float2bfloat16_rn(accumulator);
108
+ }
109
+ }
110
+
111
+ } // namespace
112
+
113
+ torch::Tensor smallm_nvfp4_linear_cuda(
114
+ const torch::Tensor& input,
115
+ const torch::Tensor& packed_weight,
116
+ const torch::Tensor& weight_block_scales,
117
+ const torch::Tensor& weight_tensor_scale,
118
+ const std::optional<torch::Tensor>& bias) {
119
+ const auto device = input.device();
120
+ c10::cuda::CUDAGuard guard(device);
121
+ const int64_t out_features64 = packed_weight.size(0);
122
+ const int64_t in_features64 = packed_weight.size(1) * 2;
123
+ const int64_t logical_m64 = input.numel() / in_features64;
124
+ TORCH_CHECK(
125
+ logical_m64 > 0 &&
126
+ logical_m64 <= static_cast<int64_t>(std::numeric_limits<int>::max()),
127
+ "small-M GEMV M is out of range");
128
+ TORCH_CHECK(
129
+ out_features64 <= static_cast<int64_t>(std::numeric_limits<int>::max()) &&
130
+ in_features64 <= static_cast<int64_t>(std::numeric_limits<int>::max()),
131
+ "small-M GEMV N or K is out of range");
132
+ const int m = static_cast<int>(logical_m64);
133
+ const int n = static_cast<int>(out_features64);
134
+ const int k = static_cast<int>(in_features64);
135
+
136
+ std::vector<int64_t> output_shape = input.sizes().vec();
137
+ output_shape.back() = out_features64;
138
+ torch::Tensor output = torch::empty(
139
+ output_shape,
140
+ input.options().dtype(at::kBFloat16));
141
+ const int64_t output_count = logical_m64 * out_features64;
142
+ const int64_t block_count64 =
143
+ (output_count + kWarpsPerBlock - 1) / kWarpsPerBlock;
144
+ TORCH_CHECK(
145
+ block_count64 <= static_cast<int64_t>(std::numeric_limits<int>::max()),
146
+ "small-M GEMV grid is too large");
147
+
148
+ const auto stream =
149
+ at::cuda::getCurrentCUDAStream(device.index()).stream();
150
+ const __nv_bfloat16* bias_pointer =
151
+ bias.has_value()
152
+ ? reinterpret_cast<const __nv_bfloat16*>(bias->data_ptr())
153
+ : nullptr;
154
+ smallm_nvfp4_gemv_kernel<<<
155
+ static_cast<int>(block_count64),
156
+ kThreads,
157
+ 0,
158
+ stream>>>(
159
+ reinterpret_cast<const __nv_bfloat16*>(input.data_ptr()),
160
+ reinterpret_cast<const uint8_t*>(packed_weight.data_ptr()),
161
+ reinterpret_cast<const uint8_t*>(weight_block_scales.data_ptr()),
162
+ reinterpret_cast<const float*>(weight_tensor_scale.data_ptr()),
163
+ bias_pointer,
164
+ reinterpret_cast<__nv_bfloat16*>(output.data_ptr()),
165
+ m,
166
+ n,
167
+ k,
168
+ static_cast<int>(weight_block_scales.size(1)));
169
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
170
+ return output;
171
+ }
runtime/native/smallm_gemv/smallm_gemv.h ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+
3
+ #include <torch/extension.h>
4
+
5
+ #include <optional>
6
+
7
+ torch::Tensor smallm_nvfp4_linear_cuda(
8
+ const torch::Tensor& input,
9
+ const torch::Tensor& packed_weight,
10
+ const torch::Tensor& weight_block_scales,
11
+ const torch::Tensor& weight_tensor_scale,
12
+ const std::optional<torch::Tensor>& bias);
runtime/web/index.html ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>S2-Pro Quantized TTS</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: dark;
10
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif;
11
+ background: #0b1014;
12
+ color: #edf3f6;
13
+ }
14
+ * { box-sizing: border-box; }
15
+ body {
16
+ margin: 0;
17
+ min-height: 100vh;
18
+ background:
19
+ radial-gradient(circle at 15% 10%, #18384a 0, transparent 34rem),
20
+ linear-gradient(145deg, #080c10, #111b21);
21
+ }
22
+ main { width: min(920px, calc(100% - 32px)); margin: 0 auto; padding: 48px 0 72px; }
23
+ header { margin-bottom: 28px; }
24
+ .eyebrow { color: #68d6c0; font-size: 0.78rem; font-weight: 800; letter-spacing: 0.15em; text-transform: uppercase; }
25
+ h1 { margin: 8px 0; font-size: clamp(2rem, 6vw, 4rem); line-height: 1; letter-spacing: -0.05em; }
26
+ header p { max-width: 700px; color: #aab9c2; line-height: 1.65; }
27
+ .card { background: rgba(18, 28, 34, 0.9); border: 1px solid #2b3d47; border-radius: 18px; padding: 24px; box-shadow: 0 22px 70px #0008; }
28
+ .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
29
+ .full { grid-column: 1 / -1; }
30
+ label { display: block; margin-bottom: 8px; color: #c9d5da; font-size: 0.88rem; font-weight: 700; }
31
+ textarea, input, select {
32
+ width: 100%; border: 1px solid #354b56; border-radius: 11px; padding: 12px 13px;
33
+ background: #0a1217; color: #f4fafc; font: inherit; outline: none;
34
+ }
35
+ textarea { min-height: 108px; resize: vertical; line-height: 1.5; }
36
+ textarea:focus, input:focus, select:focus { border-color: #68d6c0; box-shadow: 0 0 0 3px #68d6c022; }
37
+ .hint { margin: 7px 0 0; color: #82939d; font-size: 0.79rem; line-height: 1.45; }
38
+ .actions { display: flex; align-items: center; gap: 14px; margin-top: 22px; flex-wrap: wrap; }
39
+ button {
40
+ appearance: none; border: 0; border-radius: 999px; padding: 12px 22px;
41
+ color: #06100e; background: #68d6c0; font: inherit; font-weight: 850; cursor: pointer;
42
+ }
43
+ button:hover { background: #8be6d4; }
44
+ button:disabled { opacity: 0.45; cursor: wait; }
45
+ #status { color: #9fb0b9; font-size: 0.88rem; }
46
+ #status.error { color: #ff9f9f; }
47
+ .result { display: none; margin-top: 22px; padding-top: 20px; border-top: 1px solid #2b3d47; }
48
+ audio { width: 100%; }
49
+ .download { display: inline-block; margin-top: 12px; color: #68d6c0; font-weight: 700; }
50
+ .badge { display: inline-flex; align-items: center; gap: 7px; padding: 6px 10px; border: 1px solid #2b3d47; border-radius: 999px; color: #aab9c2; font-size: 0.76rem; }
51
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: #e2b34a; }
52
+ .dot.ok { background: #68d6c0; box-shadow: 0 0 12px #68d6c0; }
53
+ footer { margin-top: 22px; color: #6f8089; font-size: 0.76rem; line-height: 1.5; }
54
+ @media (max-width: 680px) { .grid { grid-template-columns: 1fr; } .full { grid-column: auto; } main { padding-top: 28px; } }
55
+ </style>
56
+ </head>
57
+ <body>
58
+ <main>
59
+ <header>
60
+ <div class="eyebrow">V1 · English balanced release</div>
61
+ <h1 id="model-title">Fish Audio S2-Pro NVFP4 Balanced</h1>
62
+ <p id="model-description">Loading the active checkpoint information…</p>
63
+ <div class="badge"><span id="health-dot" class="dot"></span><span id="health-text">Checking service…</span></div>
64
+ </header>
65
+
66
+ <section class="card">
67
+ <form id="tts-form">
68
+ <div class="grid">
69
+ <div class="full">
70
+ <label for="text">Text to speak</label>
71
+ <textarea id="text" required>A clear voice can make even a short message feel personal and memorable.</textarea>
72
+ <p class="hint">The base S2-Pro model can sound flatter than highly expressive TTS systems. Inline instructions may help some prompts, but V1 does not claim to solve that inherited limitation.</p>
73
+ </div>
74
+ <div>
75
+ <label for="reference-audio">Zero-shot reference audio</label>
76
+ <input id="reference-audio" type="file" accept="audio/*" required>
77
+ <p class="hint">Use a clean, consented 10–30 second sample with one speaker and little background noise.</p>
78
+ </div>
79
+ <div>
80
+ <label for="reference-text">Exact reference transcript</label>
81
+ <textarea id="reference-text" required placeholder="Transcribe the reference audio verbatim."></textarea>
82
+ <p class="hint">The transcript is required for reliable zero-shot conditioning.</p>
83
+ </div>
84
+ <div>
85
+ <label for="seed">Seed (optional)</label>
86
+ <input id="seed" type="number" placeholder="Random when blank">
87
+ </div>
88
+ <div>
89
+ <label for="sampling-preset">Voice-clone sampling</label>
90
+ <select id="sampling-preset">
91
+ <option value="natural" selected>Natural (recommended)</option>
92
+ <option value="api">Original API default</option>
93
+ <option value="stable">Stable identity</option>
94
+ <option value="expressive">Higher randomness (not an emotion tag)</option>
95
+ </select>
96
+ <p class="hint">Use the same seed to A/B sampling. For emotional delivery, put explicit [tag] instructions in the text.</p>
97
+ </div>
98
+ <div>
99
+ <label for="temperature">Temperature</label>
100
+ <input id="temperature" type="number" min="0.1" max="1" step="0.05" value="0.9">
101
+ </div>
102
+ <div>
103
+ <label for="top-p">Top-p</label>
104
+ <input id="top-p" type="number" min="0.1" max="1" step="0.05" value="0.9">
105
+ </div>
106
+ </div>
107
+ <div class="actions">
108
+ <button id="generate" type="submit">Generate speech</button>
109
+ <span id="status">Ready.</span>
110
+ </div>
111
+ </form>
112
+ <div id="result" class="result">
113
+ <audio id="player" controls></audio>
114
+ <a id="download" class="download" download="s2-pro-tts.wav">Download WAV</a>
115
+ </div>
116
+ </section>
117
+ <footer><strong>Built with Fish Audio.</strong> Use only voices you have permission to clone. This derivative is under the Fish Audio Research License; commercial use requires a separate written license from Fish Audio.</footer>
118
+ </main>
119
+
120
+ <script>
121
+ const form = document.querySelector('#tts-form');
122
+ const button = document.querySelector('#generate');
123
+ const status = document.querySelector('#status');
124
+ const result = document.querySelector('#result');
125
+ const player = document.querySelector('#player');
126
+ const download = document.querySelector('#download');
127
+ const samplingPreset = document.querySelector('#sampling-preset');
128
+ const temperature = document.querySelector('#temperature');
129
+ const topP = document.querySelector('#top-p');
130
+ let currentUrl = null;
131
+
132
+ const samplingProfiles = {
133
+ natural: { temperature: 0.9, topP: 0.9 },
134
+ api: { temperature: 0.8, topP: 0.8 },
135
+ stable: { temperature: 0.7, topP: 0.7 },
136
+ expressive: { temperature: 0.95, topP: 0.95 }
137
+ };
138
+
139
+ samplingPreset.addEventListener('change', () => {
140
+ const profile = samplingProfiles[samplingPreset.value];
141
+ temperature.value = profile.temperature;
142
+ topP.value = profile.topP;
143
+ });
144
+
145
+ function toBase64(buffer) {
146
+ const bytes = new Uint8Array(buffer);
147
+ const chunkSize = 32768;
148
+ let binary = '';
149
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
150
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
151
+ }
152
+ return btoa(binary);
153
+ }
154
+
155
+ function setStatus(message, isError = false) {
156
+ status.textContent = message;
157
+ status.classList.toggle('error', isError);
158
+ }
159
+
160
+ async function checkHealth() {
161
+ try {
162
+ const response = await fetch('/v1/health');
163
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
164
+ const modelResponse = await fetch('/v1/model');
165
+ if (!modelResponse.ok) throw new Error(`Model HTTP ${modelResponse.status}`);
166
+ const model = await modelResponse.json();
167
+ const isNvfp4 = model.policy === 'w4a16_gate_up_middle30_mxfp8_rest';
168
+ const isBf16 = model.policy === 'bf16_control';
169
+ document.querySelector('#model-title').textContent = model.model;
170
+ document.querySelector('#model-description').textContent = `${model.quantization}. Checkpoint: ${model.checkpoint}`;
171
+ document.querySelector('#health-dot').classList.add('ok');
172
+ document.querySelector('#health-text').textContent = isNvfp4 ? 'Checkpointed NVFP4 service online' : (isBf16 ? 'Untouched BF16 base online' : 'MXFP8 service online');
173
+ if (isNvfp4) {
174
+ samplingProfiles.natural = { temperature: 1.0, topP: 0.85 };
175
+ temperature.value = 1.0;
176
+ topP.value = 0.85;
177
+ }
178
+ } catch (error) {
179
+ document.querySelector('#health-text').textContent = 'Service unavailable';
180
+ }
181
+ }
182
+
183
+ form.addEventListener('submit', async (event) => {
184
+ event.preventDefault();
185
+ const audio = document.querySelector('#reference-audio').files[0];
186
+ const referenceText = document.querySelector('#reference-text').value.trim();
187
+ if (!audio || !referenceText) {
188
+ setStatus('Reference audio and its exact transcript are required.', true);
189
+ return;
190
+ }
191
+ button.disabled = true;
192
+ result.style.display = 'none';
193
+ setStatus('Encoding reference and generating… first use may take longer.');
194
+ try {
195
+ const seedValue = document.querySelector('#seed').value;
196
+ const payload = {
197
+ text: document.querySelector('#text').value,
198
+ references: [{ audio: toBase64(await audio.arrayBuffer()), text: referenceText }],
199
+ reference_id: null,
200
+ format: 'wav',
201
+ streaming: false,
202
+ normalize: false,
203
+ max_new_tokens: 2048,
204
+ chunk_length: 1000,
205
+ top_p: Number(topP.value),
206
+ repetition_penalty: 1.05,
207
+ temperature: Number(temperature.value),
208
+ seed: seedValue === '' ? null : Number(seedValue),
209
+ // The pinned server caches transcript text by audio hash alone.
210
+ // A fresh load prevents a prior transcript for the same WAV from
211
+ // silently conditioning this request.
212
+ use_memory_cache: 'off'
213
+ };
214
+ const headers = { 'Content-Type': 'application/json', 'Accept': 'audio/wav' };
215
+ const response = await fetch('/v1/tts', { method: 'POST', headers, body: JSON.stringify(payload) });
216
+ if (!response.ok) throw new Error((await response.text()) || `HTTP ${response.status}`);
217
+ const blob = await response.blob();
218
+ if (currentUrl) URL.revokeObjectURL(currentUrl);
219
+ currentUrl = URL.createObjectURL(blob);
220
+ player.src = currentUrl;
221
+ download.href = currentUrl;
222
+ result.style.display = 'block';
223
+ setStatus(`Complete — ${(blob.size / 1024).toFixed(0)} KiB WAV.`);
224
+ } catch (error) {
225
+ setStatus(`Generation failed: ${error.message}`, true);
226
+ } finally {
227
+ button.disabled = false;
228
+ }
229
+ });
230
+
231
+ checkHealth();
232
+ </script>
233
+ </body>
234
+ </html>
vendor/fish-speech/.github/ISSUE_TEMPLATE/bug_report.yml ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: "🕷️ Bug report"
2
+ description: |
3
+ Please follow this template carefully to ensure we can address your issue quickly.
4
+ Make sure to provide as much detail as possible, including logs and screenshots.
5
+ labels:
6
+ - bug
7
+ body:
8
+ - type: checkboxes
9
+ attributes:
10
+ label: Self Checks
11
+ description: "To ensure timely help, please confirm the following:"
12
+ options:
13
+ - label: This template is only for bug reports. For questions, please visit [Discussions](https://github.com/fishaudio/fish-speech/discussions).
14
+ required: true
15
+ - label: I have thoroughly reviewed the project documentation (installation, training, inference) but couldn't find information to solve my problem. [English](https://speech.fish.audio/) [中文](https://speech.fish.audio/zh/) [日本語](https://speech.fish.audio/ja/) [Portuguese (Brazil)](https://speech.fish.audio/pt/)
16
+ required: true
17
+ - label: I have searched for existing issues, including closed ones. [Search issues](https://github.com/fishaudio/fish-speech/issues)
18
+ required: true
19
+ - label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/fishaudio/fish-speech/issues/515)).
20
+ required: true
21
+ - label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
22
+ required: true
23
+ - label: "Please do not modify this template and fill in all required fields."
24
+ required: true
25
+ - type: dropdown
26
+ attributes:
27
+ label: Cloud or Self Hosted
28
+ multiple: true
29
+ options:
30
+ - Cloud
31
+ - Self Hosted (Docker)
32
+ - Self Hosted (Source)
33
+ validations:
34
+ required: true
35
+ - type: textarea
36
+ attributes:
37
+ label: Environment Details
38
+ description: "Provide details such as OS, Python version, and any relevant software or dependencies."
39
+ placeholder: e.g., macOS 13.5, Python 3.10, torch==2.4.1, Gradio 4.44.0
40
+ validations:
41
+ required: true
42
+ - type: textarea
43
+ attributes:
44
+ label: Steps to Reproduce
45
+ description: |
46
+ Include detailed steps, screenshots, and logs. Use the correct markdown syntax for code blocks.
47
+ placeholder: |
48
+ 1. Run the command `python -m tools.api_client -t "xxxxx"`
49
+ 2. Observe the console output error: `ModuleNotFoundError: No module named 'pyaudio'` (with screenshots or logs will be better)
50
+ validations:
51
+ required: true
52
+ - type: textarea
53
+ attributes:
54
+ label: ✔️ Expected Behavior
55
+ placeholder: Describe what you expected to happen.
56
+ validations:
57
+ required: false
58
+ - type: textarea
59
+ attributes:
60
+ label: ❌ Actual Behavior
61
+ placeholder: Describe what actually happened.
62
+ validations:
63
+ required: false
vendor/fish-speech/.github/ISSUE_TEMPLATE/config.yml ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: "\U0001F4E7 Discussions"
4
+ url: https://github.com/fishaudio/fish-speech/discussions
5
+ about: General discussions and request help from the community
vendor/fish-speech/.github/ISSUE_TEMPLATE/feature_request.yml ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: "⭐ Feature or enhancement request"
2
+ description: Propose something new.
3
+ labels:
4
+ - enhancement
5
+ body:
6
+ - type: checkboxes
7
+ attributes:
8
+ label: Self Checks
9
+ description: "To make sure we get to you in time, please check the following :)"
10
+ options:
11
+ - label: I have thoroughly reviewed the project documentation (installation, training, inference) but couldn't find any relevant information that meets my needs. [English](https://speech.fish.audio/) [中文](https://speech.fish.audio/zh/) [日本語](https://speech.fish.audio/ja/) [Portuguese (Brazil)](https://speech.fish.audio/pt/)
12
+ required: true
13
+ - label: I have searched for existing issues [search for existing issues]([https://github.com/langgenius/dify/issues](https://github.com/fishaudio/fish-speech/issues)), including closed ones.
14
+ required: true
15
+ - label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/fishaudio/fish-speech/issues/515)).
16
+ required: true
17
+ - label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
18
+ required: true
19
+ - label: "Please do not modify this template :) and fill in all the required fields."
20
+ required: true
21
+
22
+ - type: textarea
23
+ attributes:
24
+ label: 1. Is this request related to a challenge you're experiencing? Tell us your story.
25
+ description: |
26
+ Describe the specific problem or scenario you’re facing in detail. For example:
27
+ *"I was trying to use [feature] for [specific task], but encountered [issue]. This was frustrating because...."*
28
+ placeholder: Please describe the situation in as much detail as possible.
29
+ validations:
30
+ required: true
31
+
32
+ - type: textarea
33
+ attributes:
34
+ label: 2. What is your suggested solution?
35
+ description: |
36
+ Provide a clear description of the feature or enhancement you'd like to propose.
37
+ How would this feature solve your issue or improve the project?
38
+ placeholder: Describe your idea or proposed solution here.
39
+ validations:
40
+ required: true
41
+
42
+ - type: textarea
43
+ attributes:
44
+ label: 3. Additional context or comments
45
+ description: |
46
+ Any other relevant information, links, documents, or screenshots that provide clarity.
47
+ Use this section for anything not covered above.
48
+ placeholder: Add any extra details here.
49
+ validations:
50
+ required: false
51
+
52
+ - type: checkboxes
53
+ attributes:
54
+ label: 4. Can you help us with this feature?
55
+ description: |
56
+ Let us know if you're interested in contributing. This is not a commitment but a way to express interest in collaboration.
57
+ options:
58
+ - label: I am interested in contributing to this feature.
59
+ required: false
60
+
61
+ - type: markdown
62
+ attributes:
63
+ value: |
64
+ **Note:** Please submit only one request per issue to keep discussions focused and manageable.
vendor/fish-speech/.github/workflows/build-docker-image.yml ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Build Docker Images
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ tags:
8
+ - "v*"
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest-16c64g
13
+ strategy:
14
+ matrix:
15
+ target: [webui, server]
16
+ backend: [cuda, cpu]
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Set up Docker Buildx
21
+ uses: docker/setup-buildx-action@v3
22
+
23
+ - name: Get Version
24
+ run: |
25
+ if [[ $GITHUB_REF == refs/tags/v* ]]; then
26
+ version=$(basename ${GITHUB_REF})
27
+ else
28
+ version=nightly
29
+ fi
30
+ echo "version=${version}" >> $GITHUB_ENV
31
+ echo "Current version: ${version}"
32
+
33
+ - name: Login to Docker Hub
34
+ uses: docker/login-action@v3
35
+ with:
36
+ username: ${{ secrets.DOCKER_USER }}
37
+ password: ${{ secrets.DOCKER_PAT }}
38
+
39
+ - name: Set platform for CPU builds
40
+ id: platform
41
+ run: |
42
+ if [ "${{ matrix.backend }}" = "cpu" ]; then
43
+ echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
44
+ else
45
+ echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
46
+ fi
47
+
48
+ - name: Build and Push ${{ matrix.target }}-${{ matrix.backend }} Image
49
+ uses: docker/build-push-action@v6
50
+ with:
51
+ context: .
52
+ file: docker/Dockerfile
53
+ platforms: ${{ steps.platform.outputs.platforms }}
54
+ push: true
55
+ target: ${{ matrix.target }}
56
+ build-args: |
57
+ BACKEND=${{ matrix.backend }}
58
+ UV_EXTRA=${{ matrix.backend == 'cuda' && 'cu126' || 'cpu' }}
59
+ tags: |
60
+ fishaudio/fish-speech:${{ matrix.target }}-${{ matrix.backend }}-${{ env.version }}
61
+ fishaudio/fish-speech:${{ matrix.target }}-${{ matrix.backend }}
62
+ ${{ (matrix.target == 'webui' && matrix.backend == 'cuda') && format('fishaudio/fish-speech:{0}', env.version) || '' }}
63
+ ${{ (matrix.target == 'webui' && matrix.backend == 'cuda') && 'fishaudio/fish-speech:latest' || '' }}
64
+ outputs: type=image,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true
65
+ cache-from: type=registry,ref=fishaudio/fish-speech:${{ matrix.target }}-${{ matrix.backend }}
66
+ cache-to: type=inline
67
+
68
+ update-readme:
69
+ runs-on: ubuntu-latest
70
+ needs: build
71
+ if: github.ref == 'refs/heads/main'
72
+ steps:
73
+ - name: Push README to Dockerhub
74
+ uses: peter-evans/dockerhub-description@v4
75
+ with:
76
+ username: ${{ secrets.DOCKER_USER }}
77
+ password: ${{ secrets.DOCKER_PAT }}
78
+ repository: fishaudio/fish-speech
vendor/fish-speech/.github/workflows/docs.yml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: docs
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ paths:
7
+ - 'docs/**'
8
+ - 'mkdocs.yml'
9
+
10
+ permissions:
11
+ contents: write
12
+
13
+ jobs:
14
+ deploy:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - name: Configure Git Credentials
19
+ run: |
20
+ git config user.name github-actions[bot]
21
+ git config user.email 41898282+github-actions[bot]@users.noreply.github.com
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: 3.x
25
+ - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
26
+ - uses: actions/cache@v4
27
+ with:
28
+ key: mkdocs-material-${{ env.cache_id }}
29
+ path: .cache
30
+ restore-keys: |
31
+ mkdocs-material-
32
+ - run: pip install -r docs/requirements.txt
33
+ - run: mkdocs gh-deploy --force
vendor/fish-speech/.github/workflows/stale.yml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Close inactive issues
2
+ on:
3
+ schedule:
4
+ - cron: "0 0 * * *"
5
+
6
+ jobs:
7
+ close-issues:
8
+ runs-on: ubuntu-latest
9
+ permissions:
10
+ issues: write
11
+ pull-requests: write
12
+ steps:
13
+ - uses: actions/stale@v9
14
+ with:
15
+ days-before-issue-stale: 30
16
+ days-before-issue-close: 14
17
+ stale-issue-label: "stale"
18
+ stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
19
+ close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
20
+ days-before-pr-stale: 30
21
+ days-before-pr-close: 30
22
+ stale-pr-label: "stale"
23
+ stale-pr-message: "This PR is stale because it has been open for 30 days with no activity."
24
+ close-pr-message: "This PR was closed because it has been inactive for 30 days since being marked as stale."
25
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
vendor/fish-speech/awesome_webui/public/vite.svg ADDED
vendor/fish-speech/awesome_webui/src/App.tsx ADDED
@@ -0,0 +1,1185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import {
3
+ AudioLines,
4
+ ChevronDown,
5
+ CircleAlert,
6
+ Copy,
7
+ Download,
8
+ FileText,
9
+ Info,
10
+ LoaderCircle,
11
+ Plus,
12
+ Settings2,
13
+ Upload,
14
+ } from 'lucide-react'
15
+
16
+ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
17
+ import { Badge } from '@/components/ui/badge'
18
+ import { Button } from '@/components/ui/button'
19
+ import {
20
+ Card,
21
+ CardContent,
22
+ CardDescription,
23
+ CardHeader,
24
+ CardTitle,
25
+ } from '@/components/ui/card'
26
+ import {
27
+ Collapsible,
28
+ CollapsibleContent,
29
+ CollapsibleTrigger,
30
+ } from '@/components/ui/collapsible'
31
+ import {
32
+ Dialog,
33
+ DialogContent,
34
+ DialogDescription,
35
+ DialogFooter,
36
+ DialogHeader,
37
+ DialogTitle,
38
+ } from '@/components/ui/dialog'
39
+ import { Label } from '@/components/ui/label'
40
+ import { ScrollArea } from '@/components/ui/scroll-area'
41
+ import { Separator } from '@/components/ui/separator'
42
+ import { Slider } from '@/components/ui/slider'
43
+ import { Switch } from '@/components/ui/switch'
44
+ import { Textarea } from '@/components/ui/textarea'
45
+ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
46
+
47
+ type AudioFormat = 'mp3' | 'wav' | 'pcm' | 'opus'
48
+ type LatencyMode = 'normal' | 'balanced'
49
+
50
+ const defaultInputText = `[excited, joyful tone] We're going to DISNEY WORLD! [squeal of delight] I've been saving for [emphasis] three years [breathless] and finally, FINALLY we can go! The look on your face right now is worth every extra shift I worked!
51
+ [angry] After everything we've been through [break] I can't believe you would [emphasize] betray me like this. I gave you EVERYTHING! And now I'm left with nothing but memories and broken promises!`
52
+
53
+
54
+ type ControlsState = {
55
+ chunkLength: number
56
+ maxNewTokens: number
57
+ temperature: number
58
+ topP: number
59
+ repetitionPenalty: number
60
+ normalize: boolean
61
+ format: AudioFormat
62
+ latency: LatencyMode
63
+ }
64
+
65
+ type Metrics = {
66
+ textLength: number
67
+ ttftMs: number
68
+ receivedKb: number
69
+ }
70
+
71
+ type StatusState = {
72
+ tone: 'error' | 'info'
73
+ message: string
74
+ }
75
+
76
+ type ReferenceItem = {
77
+ id: number
78
+ name: string
79
+ audio: ArrayBuffer
80
+ text: string
81
+ previewUrl: string
82
+ }
83
+
84
+ type SpeakerGroup = {
85
+ id: number
86
+ references: ReferenceItem[]
87
+ }
88
+
89
+ type PendingReference = {
90
+ mode: 'create' | 'edit'
91
+ speakerId: number
92
+ referenceId?: number
93
+ name: string
94
+ audio?: ArrayBuffer
95
+ text: string
96
+ }
97
+
98
+ const initialControls: ControlsState = {
99
+ chunkLength: 1000,
100
+ maxNewTokens: 2048,
101
+ temperature: 0.9,
102
+ topP: 0.9,
103
+ repetitionPenalty: 1.05,
104
+ normalize: false,
105
+ format: 'mp3',
106
+ latency: 'normal',
107
+ }
108
+
109
+ const formatMimeMap: Record<AudioFormat, string> = {
110
+ mp3: 'audio/mpeg',
111
+ wav: 'audio/wav',
112
+ pcm: 'audio/pcm',
113
+ opus: 'audio/opus',
114
+ }
115
+
116
+ function createId() {
117
+ return Date.now() + Math.floor(Math.random() * 100000)
118
+ }
119
+
120
+ function arrayBufferToBase64(buffer: ArrayBuffer): string {
121
+ const bytes = new Uint8Array(buffer)
122
+ let binary = ''
123
+ for (let i = 0; i < bytes.byteLength; i++) {
124
+ binary += String.fromCharCode(bytes[i])
125
+ }
126
+ return btoa(binary)
127
+ }
128
+
129
+ function createSpeakerGroup(): SpeakerGroup {
130
+ return {
131
+ id: createId(),
132
+ references: [],
133
+ }
134
+ }
135
+
136
+ const initialSpeakerGroup = createSpeakerGroup()
137
+
138
+ function buildReferencesPayload(
139
+ speakerGroups: SpeakerGroup[],
140
+ includeBinaryAudio: boolean,
141
+ ) {
142
+ return speakerGroups.flatMap((speakerGroup) =>
143
+ speakerGroup.references.map((reference) => ({
144
+ text: reference.text,
145
+ audio: includeBinaryAudio
146
+ ? arrayBufferToBase64(reference.audio)
147
+ : '<audio binary data>',
148
+ })),
149
+ )
150
+ }
151
+
152
+ function buildPreviewPayload(
153
+ inputText: string,
154
+ controls: ControlsState,
155
+ speakerGroups: SpeakerGroup[],
156
+ ) {
157
+ return {
158
+ text: inputText,
159
+ chunk_length: controls.chunkLength,
160
+ max_new_tokens: controls.maxNewTokens,
161
+ format: controls.format,
162
+ latency: controls.latency,
163
+ normalize: controls.normalize,
164
+ references: buildReferencesPayload(speakerGroups, false),
165
+ temperature: controls.temperature,
166
+ top_p: controls.topP,
167
+ repetition_penalty: controls.repetitionPenalty,
168
+ }
169
+ }
170
+
171
+ function buildRequestPayload(
172
+ inputText: string,
173
+ controls: ControlsState,
174
+ speakerGroups: SpeakerGroup[],
175
+ ) {
176
+ return {
177
+ text: inputText,
178
+ chunk_length: controls.chunkLength,
179
+ max_new_tokens: controls.maxNewTokens,
180
+ format: controls.format,
181
+ latency: controls.latency,
182
+ normalize: controls.normalize,
183
+ references: buildReferencesPayload(speakerGroups, true),
184
+ temperature: controls.temperature,
185
+ top_p: controls.topP,
186
+ repetition_penalty: controls.repetitionPenalty,
187
+ }
188
+ }
189
+
190
+ function createFileName(inputText: string) {
191
+ const safePrefix = inputText.trim().replace(/\s+/g, '-').slice(0, 24) || 'tts'
192
+ return safePrefix
193
+ }
194
+
195
+ function getErrorMessage(error: unknown) {
196
+ return error instanceof Error ? error.message : 'Unknown error'
197
+ }
198
+
199
+ function waitForSourceBuffer(sourceBuffer: SourceBuffer) {
200
+ if (!sourceBuffer.updating) {
201
+ return Promise.resolve()
202
+ }
203
+
204
+ return new Promise<void>((resolve) => {
205
+ const handleUpdateEnd = () => {
206
+ sourceBuffer.removeEventListener('updateend', handleUpdateEnd)
207
+ resolve()
208
+ }
209
+
210
+ sourceBuffer.addEventListener('updateend', handleUpdateEnd)
211
+ })
212
+ }
213
+
214
+ function canUseStreamingPlayback(format: AudioFormat) {
215
+ const mime = formatMimeMap[format]
216
+ return typeof window.MediaSource !== 'undefined' && MediaSource.isTypeSupported(mime)
217
+ }
218
+
219
+ type SettingSliderProps = {
220
+ label: string
221
+ value: number
222
+ min: number
223
+ max: number
224
+ step?: number
225
+ onValueChange: (value: number) => void
226
+ formatValue?: (value: number) => string
227
+ }
228
+
229
+ function SettingSlider({
230
+ label,
231
+ value,
232
+ min,
233
+ max,
234
+ step = 1,
235
+ onValueChange,
236
+ formatValue,
237
+ }: SettingSliderProps) {
238
+ return (
239
+ <div className="space-y-3">
240
+ <div className="flex items-center justify-between gap-4">
241
+ <Label>{label}</Label>
242
+ <span className="text-sm text-muted-foreground">
243
+ {formatValue ? formatValue(value) : value}
244
+ </span>
245
+ </div>
246
+ <Slider
247
+ value={[value]}
248
+ min={min}
249
+ max={max}
250
+ step={step}
251
+ onValueChange={(nextValue) => {
252
+ const current = nextValue[0]
253
+ if (typeof current === 'number') {
254
+ onValueChange(current)
255
+ }
256
+ }}
257
+ />
258
+ </div>
259
+ )
260
+ }
261
+
262
+ function App() {
263
+ const [inputText, setInputText] = useState(defaultInputText)
264
+ const [controls, setControls] = useState(initialControls)
265
+ const [speakerGroups, setSpeakerGroups] = useState<SpeakerGroup[]>([initialSpeakerGroup])
266
+ const [pendingReference, setPendingReference] = useState<PendingReference | null>(null)
267
+ const [openSpeakerIds, setOpenSpeakerIds] = useState<number[]>([initialSpeakerGroup.id])
268
+ const [metrics, setMetrics] = useState<Metrics | null>(null)
269
+ const [isGenerating, setIsGenerating] = useState(false)
270
+ const [copyLabel, setCopyLabel] = useState('Copy')
271
+ const [isRequestPreviewOpen, setIsRequestPreviewOpen] = useState(false)
272
+ const [statusMessage, setStatusMessage] = useState<StatusState | null>(null)
273
+ const [downloadUrl, setDownloadUrl] = useState<string | null>(null)
274
+ const [downloadName, setDownloadName] = useState('generated-audio.mp3')
275
+
276
+ const audioRef = useRef<HTMLAudioElement | null>(null)
277
+ const fileInputRef = useRef<HTMLInputElement | null>(null)
278
+ const speakerGroupsRef = useRef<SpeakerGroup[]>([])
279
+ const uploadTargetSpeakerIdRef = useRef<number | null>(null)
280
+ const downloadUrlRef = useRef<string | null>(null)
281
+ const mediaSourceUrlRef = useRef<string | null>(null)
282
+
283
+ speakerGroupsRef.current = speakerGroups
284
+
285
+ useEffect(() => {
286
+ return () => {
287
+ speakerGroupsRef.current.forEach((speakerGroup) => {
288
+ speakerGroup.references.forEach((reference) => {
289
+ URL.revokeObjectURL(reference.previewUrl)
290
+ })
291
+ })
292
+
293
+ if (downloadUrlRef.current) {
294
+ URL.revokeObjectURL(downloadUrlRef.current)
295
+ }
296
+
297
+ if (mediaSourceUrlRef.current) {
298
+ URL.revokeObjectURL(mediaSourceUrlRef.current)
299
+ }
300
+ }
301
+ }, [])
302
+
303
+ function addSpeaker() {
304
+ const nextSpeaker = createSpeakerGroup()
305
+ setSpeakerGroups((current) => [...current, nextSpeaker])
306
+ setOpenSpeakerIds((current) => [...current, nextSpeaker.id])
307
+ }
308
+
309
+ function removeSpeaker(speakerId: number) {
310
+ setSpeakerGroups((current) => {
311
+ const targetSpeaker = current.find((speakerGroup) => speakerGroup.id === speakerId)
312
+ if (targetSpeaker) {
313
+ targetSpeaker.references.forEach((reference) => {
314
+ URL.revokeObjectURL(reference.previewUrl)
315
+ })
316
+ }
317
+
318
+ const next = current.filter((speakerGroup) => speakerGroup.id !== speakerId)
319
+ return next.length > 0 ? next : [createSpeakerGroup()]
320
+ })
321
+ setOpenSpeakerIds((current) => current.filter((currentSpeakerId) => currentSpeakerId !== speakerId))
322
+
323
+ if (pendingReference?.speakerId === speakerId) {
324
+ setPendingReference(null)
325
+ }
326
+ }
327
+
328
+ function addReference(speakerId: number, name: string, audio: ArrayBuffer, text: string) {
329
+ const previewUrl = URL.createObjectURL(new Blob([audio], { type: formatMimeMap.mp3 }))
330
+
331
+ setSpeakerGroups((current) =>
332
+ current.map((speakerGroup) =>
333
+ speakerGroup.id === speakerId
334
+ ? {
335
+ ...speakerGroup,
336
+ references: [
337
+ ...speakerGroup.references,
338
+ {
339
+ id: createId(),
340
+ name,
341
+ audio,
342
+ text,
343
+ previewUrl,
344
+ },
345
+ ],
346
+ }
347
+ : speakerGroup,
348
+ ),
349
+ )
350
+ }
351
+
352
+ function removeReference(speakerId: number, referenceId: number) {
353
+ setSpeakerGroups((current) =>
354
+ current.map((speakerGroup) => {
355
+ if (speakerGroup.id !== speakerId) {
356
+ return speakerGroup
357
+ }
358
+
359
+ return {
360
+ ...speakerGroup,
361
+ references: speakerGroup.references.filter((reference) => {
362
+ if (reference.id === referenceId) {
363
+ URL.revokeObjectURL(reference.previewUrl)
364
+ return false
365
+ }
366
+
367
+ return true
368
+ }),
369
+ }
370
+ }),
371
+ )
372
+ }
373
+
374
+ function updateReferenceText(speakerId: number, referenceId: number, text: string) {
375
+ setSpeakerGroups((current) =>
376
+ current.map((speakerGroup) =>
377
+ speakerGroup.id === speakerId
378
+ ? {
379
+ ...speakerGroup,
380
+ references: speakerGroup.references.map((reference) =>
381
+ reference.id === referenceId ? { ...reference, text } : reference,
382
+ ),
383
+ }
384
+ : speakerGroup,
385
+ ),
386
+ )
387
+ }
388
+
389
+ function clearDownloadUrl() {
390
+ if (downloadUrlRef.current) {
391
+ URL.revokeObjectURL(downloadUrlRef.current)
392
+ downloadUrlRef.current = null
393
+ }
394
+
395
+ setDownloadUrl(null)
396
+ }
397
+
398
+ function clearMediaSourceUrl() {
399
+ if (mediaSourceUrlRef.current) {
400
+ URL.revokeObjectURL(mediaSourceUrlRef.current)
401
+ mediaSourceUrlRef.current = null
402
+ }
403
+ }
404
+
405
+ async function handleReferenceUpload(event: React.ChangeEvent<HTMLInputElement>) {
406
+ const file = event.target.files?.[0]
407
+ const speakerId = uploadTargetSpeakerIdRef.current
408
+ event.target.value = ''
409
+ uploadTargetSpeakerIdRef.current = null
410
+
411
+ if (!file || typeof speakerId !== 'number') {
412
+ return
413
+ }
414
+
415
+ const audio = await file.arrayBuffer()
416
+ setPendingReference({
417
+ mode: 'create',
418
+ speakerId,
419
+ name: file.name,
420
+ audio,
421
+ text: '',
422
+ })
423
+ }
424
+
425
+ function savePendingReference() {
426
+ if (!pendingReference) {
427
+ return
428
+ }
429
+
430
+ if (pendingReference.mode === 'create' && pendingReference.audio) {
431
+ addReference(
432
+ pendingReference.speakerId,
433
+ pendingReference.name,
434
+ pendingReference.audio,
435
+ pendingReference.text,
436
+ )
437
+ }
438
+
439
+ if (pendingReference.mode === 'edit' && typeof pendingReference.referenceId === 'number') {
440
+ updateReferenceText(
441
+ pendingReference.speakerId,
442
+ pendingReference.referenceId,
443
+ pendingReference.text,
444
+ )
445
+ }
446
+
447
+ setPendingReference(null)
448
+ setStatusMessage(null)
449
+ }
450
+
451
+ async function copyRequestPreview() {
452
+ const requestPreview = JSON.stringify(
453
+ buildPreviewPayload(inputText, controls, speakerGroups),
454
+ null,
455
+ 2,
456
+ )
457
+
458
+ try {
459
+ await navigator.clipboard.writeText(requestPreview)
460
+ setCopyLabel('Copied')
461
+ window.setTimeout(() => setCopyLabel('Copy'), 2000)
462
+ } catch (error) {
463
+ setStatusMessage({
464
+ tone: 'error',
465
+ message: `Failed to copy request preview: ${getErrorMessage(error)}`,
466
+ })
467
+ }
468
+ }
469
+
470
+ async function handleGenerateAudio() {
471
+ const audioElement = audioRef.current
472
+ if (!audioElement) {
473
+ return
474
+ }
475
+
476
+ const mime = formatMimeMap[controls.format]
477
+ const useStreamingPlayback = canUseStreamingPlayback(controls.format)
478
+
479
+ clearDownloadUrl()
480
+ clearMediaSourceUrl()
481
+ setMetrics(null)
482
+ setStatusMessage(null)
483
+ setIsGenerating(true)
484
+
485
+ try {
486
+ const response = await fetch('/v1/tts', {
487
+ method: 'POST',
488
+ headers: {
489
+ 'Content-Type': 'application/json',
490
+ },
491
+ body: JSON.stringify(buildRequestPayload(inputText, controls, speakerGroups)),
492
+ })
493
+
494
+ if (!response.ok || !response.body) {
495
+ throw new Error('Failed to generate audio')
496
+ }
497
+
498
+ const reader = response.body.getReader()
499
+ let mediaSource: MediaSource | null = null
500
+
501
+ if (useStreamingPlayback) {
502
+ mediaSource = new MediaSource()
503
+ const streamUrl = URL.createObjectURL(mediaSource)
504
+ mediaSourceUrlRef.current = streamUrl
505
+ audioElement.src = streamUrl
506
+ } else {
507
+ audioElement.removeAttribute('src')
508
+ audioElement.load()
509
+ }
510
+
511
+ const allChunks: ArrayBuffer[] = []
512
+ const playQueue: ArrayBuffer[] = []
513
+ let sourceBuffer: SourceBuffer | null = null
514
+ let readingDone = false
515
+ let receivedLength = 0
516
+ let ttftMs = -1
517
+ const startTime = performance.now()
518
+
519
+ if (mediaSource) {
520
+ const sourceReady = new Promise<void>((resolve, reject) => {
521
+ mediaSource.addEventListener(
522
+ 'sourceopen',
523
+ () => {
524
+ try {
525
+ sourceBuffer = mediaSource.addSourceBuffer(mime)
526
+
527
+ const processQueue = async () => {
528
+ if (!sourceBuffer || !mediaSource) {
529
+ return
530
+ }
531
+
532
+ while (true) {
533
+ if (readingDone && playQueue.length === 0) {
534
+ await waitForSourceBuffer(sourceBuffer)
535
+ if (mediaSource.readyState === 'open') {
536
+ mediaSource.endOfStream()
537
+ }
538
+ break
539
+ }
540
+
541
+ const chunk = playQueue.shift()
542
+ if (!chunk) {
543
+ await new Promise<void>((resolveSleep) => {
544
+ window.setTimeout(resolveSleep, 50)
545
+ })
546
+ continue
547
+ }
548
+
549
+ await waitForSourceBuffer(sourceBuffer)
550
+ sourceBuffer.appendBuffer(chunk)
551
+ await waitForSourceBuffer(sourceBuffer)
552
+ }
553
+ }
554
+
555
+ void processQueue()
556
+ resolve()
557
+ } catch (error) {
558
+ reject(error)
559
+ }
560
+ },
561
+ { once: true },
562
+ )
563
+ })
564
+
565
+ await sourceReady
566
+ }
567
+
568
+ while (true) {
569
+ const { done, value } = await reader.read()
570
+ if (done) {
571
+ readingDone = true
572
+ break
573
+ }
574
+
575
+ receivedLength += value.byteLength
576
+
577
+ if (ttftMs < 0) {
578
+ ttftMs = performance.now() - startTime
579
+ }
580
+
581
+ setMetrics({
582
+ textLength: inputText.length,
583
+ ttftMs,
584
+ receivedKb: Math.round(receivedLength / 1024),
585
+ })
586
+
587
+ const chunk = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength)
588
+ playQueue.push(chunk)
589
+ allChunks.push(chunk)
590
+
591
+ if (useStreamingPlayback && audioElement.paused) {
592
+ void audioElement.play().catch(() => undefined)
593
+ }
594
+ }
595
+
596
+ const audioBlob = new Blob(allChunks, { type: mime })
597
+ const nextDownloadUrl = URL.createObjectURL(audioBlob)
598
+ downloadUrlRef.current = nextDownloadUrl
599
+ setDownloadUrl(nextDownloadUrl)
600
+ setDownloadName(`${createFileName(inputText)}.${controls.format}`)
601
+
602
+ if (!useStreamingPlayback) {
603
+ audioElement.src = nextDownloadUrl
604
+ audioElement.load()
605
+ setStatusMessage({
606
+ tone: 'info',
607
+ message: `Format "${controls.format}" is not supported for in-browser playback. The file is ready to download after generation completes.`,
608
+ })
609
+ }
610
+ } catch (error) {
611
+ setStatusMessage({
612
+ tone: 'error',
613
+ message: `Audio generation failed: ${getErrorMessage(error)}`,
614
+ })
615
+ } finally {
616
+ setIsGenerating(false)
617
+ }
618
+ }
619
+
620
+ const requestPreview = JSON.stringify(
621
+ buildPreviewPayload(inputText, controls, speakerGroups),
622
+ null,
623
+ 2,
624
+ )
625
+
626
+ const totalReferenceCount = speakerGroups.reduce(
627
+ (count, speakerGroup) => count + speakerGroup.references.length,
628
+ 0,
629
+ )
630
+
631
+ return (
632
+ <main className="min-h-screen bg-zinc-50">
633
+ <div className="mx-auto max-w-[1600px] px-3 py-3 sm:px-4 lg:px-5">
634
+ <div className="grid gap-4 xl:h-[calc(100vh-1.5rem)] xl:grid-cols-[minmax(0,1fr)_460px]">
635
+ <section className="grid gap-4 xl:min-h-0 xl:grid-rows-[minmax(0,1fr)_auto]">
636
+ <Card className="rounded-xl border-zinc-200 bg-white shadow-none xl:min-h-0 xl:flex xl:flex-col">
637
+ <CardHeader className="space-y-1 border-b border-zinc-100 px-4 py-4">
638
+ <div className="flex items-center gap-2 text-zinc-700">
639
+ <FileText className="size-4" />
640
+ <CardTitle>Input</CardTitle>
641
+ </div>
642
+ <CardDescription>
643
+ Enter the text to synthesize and inspect the outgoing request payload.
644
+ </CardDescription>
645
+ </CardHeader>
646
+ <CardContent className="space-y-4 px-4 pt-4 xl:min-h-0 xl:flex-1 xl:overflow-y-auto">
647
+ <div className="space-y-2">
648
+ <Label htmlFor="inputText">Input Text</Label>
649
+ <Textarea
650
+ id="inputText"
651
+ value={inputText}
652
+ onChange={(event) => setInputText(event.target.value)}
653
+ placeholder="Enter text to synthesize"
654
+ className="min-h-[220px] resize-y rounded-xl border-zinc-200 bg-white p-3 text-sm shadow-none focus-visible:ring-zinc-300 xl:min-h-[260px]"
655
+ />
656
+ </div>
657
+
658
+ <Collapsible open={isRequestPreviewOpen} onOpenChange={setIsRequestPreviewOpen}>
659
+ <div className="rounded-xl border border-zinc-200 bg-zinc-50">
660
+ <div className="flex flex-col gap-2 p-3 sm:flex-row sm:items-center sm:justify-between">
661
+ <div>
662
+ <div className="text-sm font-medium text-zinc-900">Request Preview</div>
663
+ <div className="text-xs text-zinc-500">
664
+ Live snapshot of the payload sent to the backend.
665
+ </div>
666
+ </div>
667
+ <div className="flex items-center gap-2">
668
+ <Button
669
+ type="button"
670
+ variant="ghost"
671
+ size="sm"
672
+ className="border border-zinc-200 bg-white text-zinc-700 hover:bg-zinc-100"
673
+ onClick={copyRequestPreview}
674
+ >
675
+ <Copy className="size-3.5" />
676
+ {copyLabel}
677
+ </Button>
678
+ <CollapsibleTrigger asChild>
679
+ <Button
680
+ type="button"
681
+ variant="ghost"
682
+ size="sm"
683
+ className="border border-zinc-200 bg-white text-zinc-700 hover:bg-zinc-100"
684
+ >
685
+ {isRequestPreviewOpen ? 'Collapse' : 'Expand'}
686
+ <ChevronDown
687
+ className={`size-4 transition-transform ${
688
+ isRequestPreviewOpen ? 'rotate-180' : ''
689
+ }`}
690
+ />
691
+ </Button>
692
+ </CollapsibleTrigger>
693
+ </div>
694
+ </div>
695
+ <CollapsibleContent>
696
+ <Separator className="bg-zinc-200" />
697
+ <div className="p-3 pt-3">
698
+ <ScrollArea className="h-56 min-w-0 rounded-lg border border-zinc-200 bg-white">
699
+ <pre className="max-w-full whitespace-pre-wrap break-all p-3 text-xs leading-5 text-zinc-700">
700
+ {requestPreview}
701
+ </pre>
702
+ </ScrollArea>
703
+ </div>
704
+ </CollapsibleContent>
705
+ </div>
706
+ </Collapsible>
707
+
708
+ <div className="space-y-4">
709
+ <Button
710
+ type="button"
711
+ size="lg"
712
+ className="h-11 rounded-lg bg-zinc-900 text-white hover:bg-zinc-800"
713
+ onClick={handleGenerateAudio}
714
+ disabled={isGenerating}
715
+ >
716
+ {isGenerating ? (
717
+ <LoaderCircle className="size-4 animate-spin" />
718
+ ) : (
719
+ <AudioLines className="size-4" />
720
+ )}
721
+ {isGenerating ? 'Generating Audio...' : 'Generate Audio'}
722
+ </Button>
723
+
724
+ {statusMessage ? (
725
+ <Alert
726
+ variant={statusMessage.tone === 'error' ? 'destructive' : 'warning'}
727
+ className="rounded-lg"
728
+ >
729
+ <div className="flex items-start gap-3">
730
+ {statusMessage.tone === 'error' ? (
731
+ <CircleAlert className="mt-0.5 size-4 shrink-0" />
732
+ ) : (
733
+ <Info className="mt-0.5 size-4 shrink-0" />
734
+ )}
735
+ <div>
736
+ <AlertTitle>
737
+ {statusMessage.tone === 'error' ? 'Error' : 'Notice'}
738
+ </AlertTitle>
739
+ <AlertDescription>{statusMessage.message}</AlertDescription>
740
+ </div>
741
+ </div>
742
+ </Alert>
743
+ ) : null}
744
+ </div>
745
+ </CardContent>
746
+ </Card>
747
+
748
+ <Card className="rounded-xl border-zinc-200 bg-white shadow-none">
749
+ <CardHeader className="space-y-1 border-b border-zinc-100 px-4 py-4">
750
+ <div className="flex items-center gap-2 text-zinc-700">
751
+ <AudioLines className="size-4" />
752
+ <CardTitle>Output</CardTitle>
753
+ </div>
754
+ <CardDescription>
755
+ Stream the result when supported, then preview or download the final file.
756
+ </CardDescription>
757
+ </CardHeader>
758
+ <CardContent className="space-y-3 px-4 pt-4">
759
+ <audio
760
+ ref={audioRef}
761
+ controls
762
+ className="w-full rounded-lg border border-zinc-200 bg-white"
763
+ />
764
+
765
+ <div className="flex flex-wrap gap-2">
766
+ {metrics ? (
767
+ <>
768
+ <Badge variant="outline" className="border-zinc-200 bg-white text-zinc-700">
769
+ Text length: {metrics.textLength}
770
+ </Badge>
771
+ <Badge variant="outline" className="border-zinc-200 bg-white text-zinc-700">
772
+ TTFT: {metrics.ttftMs.toFixed(2)} ms
773
+ </Badge>
774
+ <Badge variant="outline" className="border-zinc-200 bg-white text-zinc-700">
775
+ Received: {metrics.receivedKb} KB
776
+ </Badge>
777
+ </>
778
+ ) : (
779
+ <Badge variant="outline" className="border-zinc-200 bg-white text-zinc-500">
780
+ No output yet
781
+ </Badge>
782
+ )}
783
+ </div>
784
+
785
+ <div className="flex justify-end">
786
+ {downloadUrl ? (
787
+ <Button
788
+ asChild
789
+ variant="outline"
790
+ className="border-zinc-200 bg-white text-zinc-800 hover:bg-zinc-100"
791
+ >
792
+ <a href={downloadUrl} download={downloadName}>
793
+ <Download className="size-4" />
794
+ Download
795
+ </a>
796
+ </Button>
797
+ ) : null}
798
+ </div>
799
+ </CardContent>
800
+ </Card>
801
+ </section>
802
+
803
+ <aside className="grid gap-4 xl:min-h-0 xl:grid-rows-[minmax(0,1fr)_auto]">
804
+ <Card className="rounded-xl border-zinc-200 bg-white shadow-none xl:min-h-0 xl:flex xl:flex-col">
805
+ <CardHeader className="space-y-1 border-b border-zinc-100 px-4 py-4">
806
+ <div className="flex items-center gap-2 text-zinc-700">
807
+ <Upload className="size-4" />
808
+ <CardTitle>Reference Audio</CardTitle>
809
+ </div>
810
+ <CardDescription>
811
+ Build one or more speaker groups. Each speaker can have multiple reference clips.
812
+ </CardDescription>
813
+ </CardHeader>
814
+ <CardContent className="space-y-3 px-4 pt-4 xl:min-h-0 xl:flex xl:flex-1 xl:flex-col">
815
+ <div className="flex flex-wrap items-center justify-between gap-2">
816
+ <div className="flex items-center text-sm text-zinc-500">
817
+ {speakerGroups.length} speaker{speakerGroups.length === 1 ? '' : 's'} /{' '}
818
+ {totalReferenceCount} reference{totalReferenceCount === 1 ? '' : 's'}
819
+ </div>
820
+ <Button
821
+ type="button"
822
+ variant="outline"
823
+ className="border-zinc-200 bg-white hover:bg-zinc-100"
824
+ onClick={addSpeaker}
825
+ >
826
+ <Plus className="size-4" />
827
+ Add Speaker
828
+ </Button>
829
+ <input
830
+ ref={fileInputRef}
831
+ type="file"
832
+ accept="audio/*"
833
+ className="hidden"
834
+ onChange={handleReferenceUpload}
835
+ />
836
+ </div>
837
+
838
+ <ScrollArea className="min-h-0 rounded-md xl:h-full xl:flex-1">
839
+ <div className="space-y-2">
840
+ {speakerGroups.length > 0 ? (
841
+ speakerGroups.map((speakerGroup, speakerIndex) => (
842
+ <Collapsible
843
+ key={speakerGroup.id}
844
+ open={openSpeakerIds.includes(speakerGroup.id)}
845
+ onOpenChange={(open) => {
846
+ setOpenSpeakerIds((current) =>
847
+ open
848
+ ? [...current, speakerGroup.id]
849
+ : current.filter(
850
+ (currentSpeakerId) => currentSpeakerId !== speakerGroup.id,
851
+ ),
852
+ )
853
+ }}
854
+ >
855
+ <div className="rounded-lg border border-zinc-200 bg-white">
856
+ <div className="flex flex-col gap-2 px-3 py-3 sm:flex-row sm:items-center sm:justify-between">
857
+ <div className="min-w-0">
858
+ <div className="text-sm font-medium text-zinc-900">
859
+ Speaker {speakerIndex}
860
+ </div>
861
+ <div className="text-xs text-zinc-500">
862
+ {speakerGroup.references.length} reference
863
+ {speakerGroup.references.length === 1 ? '' : 's'}
864
+ </div>
865
+ </div>
866
+ <div className="flex flex-wrap gap-2">
867
+ <Button
868
+ type="button"
869
+ variant="outline"
870
+ size="sm"
871
+ className="h-8 border-zinc-200 bg-white px-2.5 hover:bg-zinc-100"
872
+ onClick={() => {
873
+ uploadTargetSpeakerIdRef.current = speakerGroup.id
874
+ fileInputRef.current?.click()
875
+ }}
876
+ >
877
+ <Upload className="size-4" />
878
+ Upload
879
+ </Button>
880
+ {speakerGroups.length > 1 ? (
881
+ <Button
882
+ type="button"
883
+ variant="ghost"
884
+ size="sm"
885
+ className="h-8 px-2.5 text-zinc-500 hover:bg-zinc-100 hover:text-zinc-900"
886
+ onClick={() => removeSpeaker(speakerGroup.id)}
887
+ >
888
+ Remove
889
+ </Button>
890
+ ) : null}
891
+ <CollapsibleTrigger asChild>
892
+ <Button
893
+ type="button"
894
+ variant="ghost"
895
+ size="sm"
896
+ className="h-8 px-2 text-zinc-500 hover:bg-zinc-100 hover:text-zinc-900"
897
+ >
898
+ <ChevronDown
899
+ className={`size-4 transition-transform ${
900
+ openSpeakerIds.includes(speakerGroup.id) ? 'rotate-180' : ''
901
+ }`}
902
+ />
903
+ </Button>
904
+ </CollapsibleTrigger>
905
+ </div>
906
+ </div>
907
+
908
+ <CollapsibleContent>
909
+ <Separator className="bg-zinc-200" />
910
+ <div className="space-y-2 px-3 py-2.5">
911
+ {speakerGroup.references.length > 0 ? (
912
+ speakerGroup.references.map((reference) => (
913
+ <div
914
+ key={reference.id}
915
+ className="flex flex-col gap-2 rounded-md border border-zinc-200 bg-zinc-50 p-2 sm:flex-row sm:items-center"
916
+ >
917
+ <audio
918
+ controls
919
+ src={reference.previewUrl}
920
+ className="h-9 w-full min-w-0 rounded-md border border-zinc-200 bg-white sm:flex-1"
921
+ />
922
+ <div className="flex gap-2 sm:shrink-0">
923
+ <Button
924
+ type="button"
925
+ variant="ghost"
926
+ size="sm"
927
+ className="h-8 border border-zinc-200 bg-white px-2.5 text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900"
928
+ onClick={() =>
929
+ setPendingReference({
930
+ mode: 'edit',
931
+ speakerId: speakerGroup.id,
932
+ referenceId: reference.id,
933
+ name: reference.name,
934
+ text: reference.text,
935
+ })
936
+ }
937
+ >
938
+ Edit Text
939
+ </Button>
940
+ <Button
941
+ type="button"
942
+ variant="ghost"
943
+ size="sm"
944
+ className="h-8 border border-zinc-200 bg-white px-2.5 text-zinc-500 hover:bg-zinc-100 hover:text-zinc-900"
945
+ onClick={() =>
946
+ removeReference(speakerGroup.id, reference.id)
947
+ }
948
+ >
949
+ Remove
950
+ </Button>
951
+ </div>
952
+ </div>
953
+ ))
954
+ ) : (
955
+ <div className="px-1 py-3 text-sm text-zinc-500">
956
+ No references yet.
957
+ </div>
958
+ )}
959
+ </div>
960
+ </CollapsibleContent>
961
+ </div>
962
+ </Collapsible>
963
+ ))
964
+ ) : (
965
+ <div className="rounded-lg border border-dashed border-zinc-300 bg-white p-4 text-sm text-zinc-500">
966
+ No speaker groups configured yet.
967
+ </div>
968
+ )}
969
+ </div>
970
+ </ScrollArea>
971
+ </CardContent>
972
+ </Card>
973
+
974
+ <Card className="rounded-xl border-zinc-200 bg-white shadow-none">
975
+ <CardHeader className="space-y-1 border-b border-zinc-100 px-4 py-4">
976
+ <div className="flex items-center gap-2 text-zinc-700">
977
+ <Settings2 className="size-4" />
978
+ <CardTitle>Generation Settings</CardTitle>
979
+ </div>
980
+ <CardDescription>Adjust sampling and output parameters.</CardDescription>
981
+ </CardHeader>
982
+ <CardContent className="space-y-4 px-4 pt-4">
983
+ <div className="space-y-2">
984
+ <Label>Latency Mode</Label>
985
+ <ToggleGroup
986
+ type="single"
987
+ value={controls.latency}
988
+ className="grid grid-cols-2 gap-2"
989
+ onValueChange={(value) => {
990
+ if (value) {
991
+ setControls((current) => ({
992
+ ...current,
993
+ latency: value as LatencyMode,
994
+ }))
995
+ }
996
+ }}
997
+ >
998
+ <ToggleGroupItem value="balanced" className="w-full">
999
+ balanced
1000
+ </ToggleGroupItem>
1001
+ <ToggleGroupItem value="normal" className="w-full">
1002
+ normal
1003
+ </ToggleGroupItem>
1004
+ </ToggleGroup>
1005
+ <p className="text-xs text-zinc-500">
1006
+ Low uses incremental local decode for faster first audio. Normal waits for the
1007
+ full LLM result, then decodes once.
1008
+ </p>
1009
+ </div>
1010
+
1011
+ <div className="space-y-2">
1012
+ <Label>Format</Label>
1013
+ <ToggleGroup
1014
+ type="single"
1015
+ value={controls.format}
1016
+ className="grid grid-cols-4 gap-2"
1017
+ onValueChange={(value) => {
1018
+ if (value) {
1019
+ setControls((current) => ({
1020
+ ...current,
1021
+ format: value as AudioFormat,
1022
+ }))
1023
+ }
1024
+ }}
1025
+ >
1026
+ <ToggleGroupItem value="mp3" className="w-full">
1027
+ mp3
1028
+ </ToggleGroupItem>
1029
+ <ToggleGroupItem value="wav" className="w-full">
1030
+ wav
1031
+ </ToggleGroupItem>
1032
+ <ToggleGroupItem value="pcm" className="w-full">
1033
+ pcm
1034
+ </ToggleGroupItem>
1035
+ <ToggleGroupItem value="opus" className="w-full">
1036
+ opus
1037
+ </ToggleGroupItem>
1038
+ </ToggleGroup>
1039
+ </div>
1040
+
1041
+ <div className="flex items-center justify-between rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2.5">
1042
+ <div className="space-y-1">
1043
+ <Label htmlFor="normalize">Normalize</Label>
1044
+ <p className="text-xs text-zinc-500">
1045
+ Normalize text before synthesis to keep input formatting consistent.
1046
+ </p>
1047
+ </div>
1048
+ <Switch
1049
+ id="normalize"
1050
+ checked={controls.normalize}
1051
+ onCheckedChange={(checked) =>
1052
+ setControls((current) => ({
1053
+ ...current,
1054
+ normalize: checked,
1055
+ }))
1056
+ }
1057
+ />
1058
+ </div>
1059
+
1060
+ <Separator className="bg-zinc-200" />
1061
+
1062
+ <SettingSlider
1063
+ label="Chunk Length"
1064
+ value={controls.chunkLength}
1065
+ min={100}
1066
+ max={1000}
1067
+ onValueChange={(value) =>
1068
+ setControls((current) => ({
1069
+ ...current,
1070
+ chunkLength: value,
1071
+ }))
1072
+ }
1073
+ />
1074
+ <SettingSlider
1075
+ label="Max New Tokens"
1076
+ value={controls.maxNewTokens}
1077
+ min={256}
1078
+ max={2048}
1079
+ onValueChange={(value) =>
1080
+ setControls((current) => ({
1081
+ ...current,
1082
+ maxNewTokens: value,
1083
+ }))
1084
+ }
1085
+ />
1086
+ <SettingSlider
1087
+ label="Temperature"
1088
+ value={controls.temperature}
1089
+ min={0.8}
1090
+ max={1}
1091
+ step={0.01}
1092
+ formatValue={(value) => value.toFixed(2)}
1093
+ onValueChange={(value) =>
1094
+ setControls((current) => ({
1095
+ ...current,
1096
+ temperature: value,
1097
+ }))
1098
+ }
1099
+ />
1100
+ <SettingSlider
1101
+ label="Top P"
1102
+ value={controls.topP}
1103
+ min={0.8}
1104
+ max={1}
1105
+ step={0.01}
1106
+ formatValue={(value) => value.toFixed(2)}
1107
+ onValueChange={(value) =>
1108
+ setControls((current) => ({
1109
+ ...current,
1110
+ topP: value,
1111
+ }))
1112
+ }
1113
+ />
1114
+ <SettingSlider
1115
+ label="Repetition Penalty"
1116
+ value={controls.repetitionPenalty}
1117
+ min={1}
1118
+ max={1.2}
1119
+ step={0.01}
1120
+ formatValue={(value) => value.toFixed(2)}
1121
+ onValueChange={(value) =>
1122
+ setControls((current) => ({
1123
+ ...current,
1124
+ repetitionPenalty: value,
1125
+ }))
1126
+ }
1127
+ />
1128
+ </CardContent>
1129
+ </Card>
1130
+ </aside>
1131
+ </div>
1132
+ </div>
1133
+
1134
+ <Dialog open={pendingReference !== null} onOpenChange={(open) => !open && setPendingReference(null)}>
1135
+ <DialogContent className="border-zinc-200 bg-white">
1136
+ <DialogHeader>
1137
+ <DialogTitle>
1138
+ {pendingReference?.mode === 'create' ? 'Save Reference Text' : 'Edit Reference Text'}
1139
+ </DialogTitle>
1140
+ <DialogDescription>
1141
+ {pendingReference
1142
+ ? `Speaker ${speakerGroups.findIndex(
1143
+ (speakerGroup) => speakerGroup.id === pendingReference.speakerId,
1144
+ )}`
1145
+ : ''}
1146
+ </DialogDescription>
1147
+ </DialogHeader>
1148
+ <div className="space-y-3">
1149
+ <div className="text-sm font-medium text-zinc-900">{pendingReference?.name}</div>
1150
+ <Textarea
1151
+ value={pendingReference?.text ?? ''}
1152
+ onChange={(event) =>
1153
+ setPendingReference((current) =>
1154
+ current
1155
+ ? {
1156
+ ...current,
1157
+ text: event.target.value,
1158
+ }
1159
+ : current,
1160
+ )
1161
+ }
1162
+ placeholder="Enter reference text"
1163
+ className="min-h-40 rounded-lg border-zinc-200 bg-white shadow-none focus-visible:ring-zinc-300"
1164
+ />
1165
+ </div>
1166
+ <DialogFooter>
1167
+ <Button type="button" variant="ghost" onClick={() => setPendingReference(null)}>
1168
+ Cancel
1169
+ </Button>
1170
+ <Button
1171
+ type="button"
1172
+ variant="outline"
1173
+ className="border-zinc-200 bg-white hover:bg-zinc-100"
1174
+ onClick={savePendingReference}
1175
+ >
1176
+ Save
1177
+ </Button>
1178
+ </DialogFooter>
1179
+ </DialogContent>
1180
+ </Dialog>
1181
+ </main>
1182
+ )
1183
+ }
1184
+
1185
+ export default App
vendor/fish-speech/awesome_webui/src/assets/react.svg ADDED
vendor/fish-speech/awesome_webui/src/components/ui/alert.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import { cva, type VariantProps } from 'class-variance-authority'
3
+
4
+ import { cn } from '@/lib/utils'
5
+
6
+ const alertVariants = cva('relative w-full rounded-lg border px-4 py-3 text-sm', {
7
+ variants: {
8
+ variant: {
9
+ default: 'bg-card text-card-foreground',
10
+ destructive: 'border-destructive/20 bg-destructive/5 text-destructive',
11
+ warning: 'border-amber-200 bg-amber-50 text-amber-900',
12
+ },
13
+ },
14
+ defaultVariants: {
15
+ variant: 'default',
16
+ },
17
+ })
18
+
19
+ function Alert({
20
+ className,
21
+ variant,
22
+ ...props
23
+ }: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
24
+ return <div role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
25
+ }
26
+
27
+ function AlertTitle({ className, ...props }: React.ComponentProps<'h5'>) {
28
+ return <h5 className={cn('mb-1 font-medium leading-none tracking-tight', className)} {...props} />
29
+ }
30
+
31
+ function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) {
32
+ return <div className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />
33
+ }
34
+
35
+ export { Alert, AlertDescription, AlertTitle }
vendor/fish-speech/awesome_webui/src/components/ui/badge.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable react-refresh/only-export-components */
2
+ import * as React from 'react'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+
5
+ import { cn } from '@/lib/utils'
6
+
7
+ const badgeVariants = cva(
8
+ 'inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium transition-colors',
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: 'border-transparent bg-primary text-primary-foreground',
13
+ secondary: 'border-transparent bg-secondary text-secondary-foreground',
14
+ outline: 'text-foreground',
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: 'default',
19
+ },
20
+ },
21
+ )
22
+
23
+ function Badge({
24
+ className,
25
+ variant,
26
+ ...props
27
+ }: React.ComponentProps<'div'> & VariantProps<typeof badgeVariants>) {
28
+ return <div className={cn(badgeVariants({ variant }), className)} {...props} />
29
+ }
30
+
31
+ export { Badge, badgeVariants }
vendor/fish-speech/awesome_webui/src/components/ui/button.tsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable react-refresh/only-export-components */
2
+ import * as React from 'react'
3
+ import { Slot } from '@radix-ui/react-slot'
4
+ import { cva, type VariantProps } from 'class-variance-authority'
5
+
6
+ import { cn } from '@/lib/utils'
7
+
8
+ const buttonVariants = cva(
9
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 outline-none focus-visible:ring-2 focus-visible:ring-ring/70 focus-visible:ring-offset-2 focus-visible:ring-offset-background',
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
14
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15
+ outline: 'border bg-card hover:bg-accent hover:text-accent-foreground',
16
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
17
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
18
+ },
19
+ size: {
20
+ default: 'h-9 px-4 py-2',
21
+ sm: 'h-8 rounded-md px-3 text-xs',
22
+ lg: 'h-11 rounded-md px-6',
23
+ icon: 'size-9',
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ variant: 'default',
28
+ size: 'default',
29
+ },
30
+ },
31
+ )
32
+
33
+ type ButtonProps = React.ComponentProps<'button'> &
34
+ VariantProps<typeof buttonVariants> & {
35
+ asChild?: boolean
36
+ }
37
+
38
+ function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
39
+ const Comp = asChild ? Slot : 'button'
40
+
41
+ return <Comp className={cn(buttonVariants({ variant, size, className }))} {...props} />
42
+ }
43
+
44
+ export { Button, buttonVariants }
vendor/fish-speech/awesome_webui/src/components/ui/card.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '@/lib/utils'
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<'div'>) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn('rounded-xl border bg-card text-card-foreground shadow-sm', className)}
10
+ {...props}
11
+ />
12
+ )
13
+ }
14
+
15
+ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
16
+ return <div className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
17
+ }
18
+
19
+ function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
20
+ return <div className={cn('text-base font-semibold leading-none tracking-tight', className)} {...props} />
21
+ }
22
+
23
+ function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
24
+ return <div className={cn('text-sm text-muted-foreground', className)} {...props} />
25
+ }
26
+
27
+ function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
28
+ return <div className={cn('p-6 pt-0', className)} {...props} />
29
+ }
30
+
31
+ export { Card, CardContent, CardDescription, CardHeader, CardTitle }
vendor/fish-speech/awesome_webui/src/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'
2
+
3
+ const Collapsible = CollapsiblePrimitive.Root
4
+ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
5
+ const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
6
+
7
+ export { Collapsible, CollapsibleContent, CollapsibleTrigger }
vendor/fish-speech/awesome_webui/src/components/ui/dialog.tsx ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import * as DialogPrimitive from '@radix-ui/react-dialog'
3
+ import { X } from 'lucide-react'
4
+
5
+ import { cn } from '@/lib/utils'
6
+
7
+ const Dialog = DialogPrimitive.Root
8
+ const DialogTrigger = DialogPrimitive.Trigger
9
+ const DialogPortal = DialogPrimitive.Portal
10
+ const DialogClose = DialogPrimitive.Close
11
+
12
+ function DialogOverlay({
13
+ className,
14
+ ...props
15
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
16
+ return (
17
+ <DialogPrimitive.Overlay
18
+ className={cn('fixed inset-0 z-50 bg-black/40', className)}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ function DialogContent({
25
+ className,
26
+ children,
27
+ ...props
28
+ }: React.ComponentProps<typeof DialogPrimitive.Content>) {
29
+ return (
30
+ <DialogPortal>
31
+ <DialogOverlay />
32
+ <DialogPrimitive.Content
33
+ className={cn(
34
+ 'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl border bg-background p-6 shadow-lg duration-200',
35
+ className,
36
+ )}
37
+ {...props}
38
+ >
39
+ {children}
40
+ <DialogClose className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring/70">
41
+ <X className="size-4" />
42
+ <span className="sr-only">Close</span>
43
+ </DialogClose>
44
+ </DialogPrimitive.Content>
45
+ </DialogPortal>
46
+ )
47
+ }
48
+
49
+ function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
50
+ return <div className={cn('flex flex-col space-y-1.5 text-left', className)} {...props} />
51
+ }
52
+
53
+ function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
54
+ return <div className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)} {...props} />
55
+ }
56
+
57
+ function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
58
+ return (
59
+ <DialogPrimitive.Title
60
+ className={cn('text-lg font-semibold leading-none tracking-tight', className)}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ function DialogDescription({
67
+ className,
68
+ ...props
69
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
70
+ return (
71
+ <DialogPrimitive.Description
72
+ className={cn('text-sm text-muted-foreground', className)}
73
+ {...props}
74
+ />
75
+ )
76
+ }
77
+
78
+ export {
79
+ Dialog,
80
+ DialogContent,
81
+ DialogDescription,
82
+ DialogFooter,
83
+ DialogHeader,
84
+ DialogTitle,
85
+ DialogTrigger,
86
+ }
vendor/fish-speech/awesome_webui/src/components/ui/label.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import * as LabelPrimitive from '@radix-ui/react-label'
3
+
4
+ import { cn } from '@/lib/utils'
5
+
6
+ function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
7
+ return (
8
+ <LabelPrimitive.Root
9
+ className={cn('text-sm font-medium leading-none', className)}
10
+ {...props}
11
+ />
12
+ )
13
+ }
14
+
15
+ export { Label }
vendor/fish-speech/awesome_webui/src/components/ui/scroll-area.tsx ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
3
+
4
+ import { cn } from '@/lib/utils'
5
+
6
+ function ScrollArea({
7
+ className,
8
+ children,
9
+ ...props
10
+ }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
11
+ return (
12
+ <ScrollAreaPrimitive.Root className={cn('relative overflow-hidden', className)} {...props}>
13
+ <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
14
+ {children}
15
+ </ScrollAreaPrimitive.Viewport>
16
+ <ScrollBar />
17
+ <ScrollAreaPrimitive.Corner />
18
+ </ScrollAreaPrimitive.Root>
19
+ )
20
+ }
21
+
22
+ function ScrollBar({
23
+ className,
24
+ orientation = 'vertical',
25
+ ...props
26
+ }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
27
+ return (
28
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
29
+ orientation={orientation}
30
+ className={cn(
31
+ 'flex touch-none select-none p-px transition-colors',
32
+ orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent',
33
+ orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent',
34
+ className,
35
+ )}
36
+ {...props}
37
+ >
38
+ <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
39
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
40
+ )
41
+ }
42
+
43
+ export { ScrollArea, ScrollBar }
vendor/fish-speech/awesome_webui/src/components/ui/separator.tsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import * as SeparatorPrimitive from '@radix-ui/react-separator'
3
+
4
+ import { cn } from '@/lib/utils'
5
+
6
+ function Separator({
7
+ className,
8
+ orientation = 'horizontal',
9
+ decorative = true,
10
+ ...props
11
+ }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
12
+ return (
13
+ <SeparatorPrimitive.Root
14
+ decorative={decorative}
15
+ orientation={orientation}
16
+ className={cn(
17
+ 'shrink-0 bg-border',
18
+ orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',
19
+ className,
20
+ )}
21
+ {...props}
22
+ />
23
+ )
24
+ }
25
+
26
+ export { Separator }
vendor/fish-speech/awesome_webui/src/components/ui/slider.tsx ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import * as SliderPrimitive from '@radix-ui/react-slider'
3
+
4
+ import { cn } from '@/lib/utils'
5
+
6
+ function Slider({
7
+ className,
8
+ ...props
9
+ }: React.ComponentProps<typeof SliderPrimitive.Root>) {
10
+ return (
11
+ <SliderPrimitive.Root
12
+ className={cn('relative flex w-full touch-none select-none items-center', className)}
13
+ {...props}
14
+ >
15
+ <SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted">
16
+ <SliderPrimitive.Range className="absolute h-full bg-primary" />
17
+ </SliderPrimitive.Track>
18
+ <SliderPrimitive.Thumb className="block size-4 rounded-full border border-primary/20 bg-background shadow-sm transition-colors focus-visible:ring-2 focus-visible:ring-ring/70 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50" />
19
+ </SliderPrimitive.Root>
20
+ )
21
+ }
22
+
23
+ export { Slider }
vendor/fish-speech/awesome_webui/src/components/ui/switch.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import * as SwitchPrimitive from '@radix-ui/react-switch'
3
+
4
+ import { cn } from '@/lib/utils'
5
+
6
+ function Switch({
7
+ className,
8
+ ...props
9
+ }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
10
+ return (
11
+ <SwitchPrimitive.Root
12
+ className={cn(
13
+ 'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-input shadow-xs transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring/70 focus-visible:ring-offset-2 focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted-foreground/30 disabled:cursor-not-allowed disabled:opacity-50',
14
+ className,
15
+ )}
16
+ {...props}
17
+ >
18
+ <SwitchPrimitive.Thumb
19
+ className={cn(
20
+ 'pointer-events-none block size-5 rounded-full bg-background shadow-sm ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0',
21
+ )}
22
+ />
23
+ </SwitchPrimitive.Root>
24
+ )
25
+ }
26
+
27
+ export { Switch }
vendor/fish-speech/awesome_webui/src/components/ui/textarea.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '@/lib/utils'
4
+
5
+ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
6
+ return (
7
+ <textarea
8
+ className={cn(
9
+ 'flex min-h-16 w-full rounded-lg border border-input bg-background px-3 py-2 text-sm shadow-xs outline-none transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring/70 disabled:cursor-not-allowed disabled:opacity-50',
10
+ className,
11
+ )}
12
+ {...props}
13
+ />
14
+ )
15
+ }
16
+
17
+ export { Textarea }
vendor/fish-speech/awesome_webui/src/components/ui/toggle-group.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+
5
+ import { cn } from '@/lib/utils'
6
+
7
+ const toggleGroupItemVariants = cva(
8
+ 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring/70 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-primary data-[state=on]:text-primary-foreground border border-border bg-card',
9
+ {
10
+ variants: {
11
+ size: {
12
+ default: 'h-9 px-3',
13
+ sm: 'h-8 px-2.5 text-xs',
14
+ lg: 'h-10 px-4',
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ size: 'default',
19
+ },
20
+ },
21
+ )
22
+
23
+ function ToggleGroup({
24
+ className,
25
+ ...props
26
+ }: React.ComponentProps<typeof ToggleGroupPrimitive.Root>) {
27
+ return (
28
+ <ToggleGroupPrimitive.Root
29
+ className={cn('flex items-center gap-2', className)}
30
+ {...props}
31
+ />
32
+ )
33
+ }
34
+
35
+ function ToggleGroupItem({
36
+ className,
37
+ size,
38
+ ...props
39
+ }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
40
+ VariantProps<typeof toggleGroupItemVariants>) {
41
+ return (
42
+ <ToggleGroupPrimitive.Item
43
+ className={cn(toggleGroupItemVariants({ size }), className)}
44
+ {...props}
45
+ />
46
+ )
47
+ }
48
+
49
+ export { ToggleGroup, ToggleGroupItem }
vendor/fish-speech/awesome_webui/src/index.css ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: 0 0% 96%;
5
+ --foreground: 240 10% 3.9%;
6
+ --card: 0 0% 100%;
7
+ --card-foreground: 240 10% 3.9%;
8
+ --popover: 0 0% 100%;
9
+ --popover-foreground: 240 10% 3.9%;
10
+ --primary: 240 5.9% 10%;
11
+ --primary-foreground: 0 0% 98%;
12
+ --secondary: 240 4.8% 95.9%;
13
+ --secondary-foreground: 240 5.9% 10%;
14
+ --muted: 240 4.8% 95.9%;
15
+ --muted-foreground: 240 3.8% 46.1%;
16
+ --accent: 240 4.8% 95.9%;
17
+ --accent-foreground: 240 5.9% 10%;
18
+ --destructive: 0 72.2% 50.6%;
19
+ --destructive-foreground: 0 0% 98%;
20
+ --border: 240 5.9% 88%;
21
+ --input: 240 5.9% 88%;
22
+ --ring: 240 5% 64.9%;
23
+ --radius: 0.75rem;
24
+ }
25
+
26
+ @theme inline {
27
+ --color-background: hsl(var(--background));
28
+ --color-foreground: hsl(var(--foreground));
29
+ --color-card: hsl(var(--card));
30
+ --color-card-foreground: hsl(var(--card-foreground));
31
+ --color-popover: hsl(var(--popover));
32
+ --color-popover-foreground: hsl(var(--popover-foreground));
33
+ --color-primary: hsl(var(--primary));
34
+ --color-primary-foreground: hsl(var(--primary-foreground));
35
+ --color-secondary: hsl(var(--secondary));
36
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
37
+ --color-muted: hsl(var(--muted));
38
+ --color-muted-foreground: hsl(var(--muted-foreground));
39
+ --color-accent: hsl(var(--accent));
40
+ --color-accent-foreground: hsl(var(--accent-foreground));
41
+ --color-destructive: hsl(var(--destructive));
42
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
43
+ --color-border: hsl(var(--border));
44
+ --color-input: hsl(var(--input));
45
+ --color-ring: hsl(var(--ring));
46
+ --radius-sm: calc(var(--radius) - 4px);
47
+ --radius-md: calc(var(--radius) - 2px);
48
+ --radius-lg: var(--radius);
49
+ --radius-xl: calc(var(--radius) + 4px);
50
+ }
51
+
52
+ @layer base {
53
+ * {
54
+ @apply border-border;
55
+ }
56
+
57
+ html {
58
+ min-width: 320px;
59
+ }
60
+
61
+ body {
62
+ @apply bg-background text-foreground antialiased;
63
+ font-family: "Inter", "Avenir Next", "Segoe UI", sans-serif;
64
+ }
65
+
66
+ button,
67
+ input,
68
+ textarea {
69
+ font: inherit;
70
+ }
71
+ }
vendor/fish-speech/awesome_webui/src/lib/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
vendor/fish-speech/awesome_webui/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
vendor/fish-speech/docs/ar/finetune.md ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # الضبط الدقيق (Fine-tuning)
2
+
3
+ من الواضح أنك عندما فتحت هذه الصفحة، لم تكن راضيًا عن أداء النموذج المدرب مسبقًا في وضع zero-shot. أنت ترغب في إجراء ضبط دقيق لنموذج لتحسين أدائه على مجموعة البيانات الخاصة بك.
4
+
5
+ في الإصدار الحالي، ما عليك سوى إجراء الضبط الدقيق لجزء 'LLAMA'.
6
+
7
+ ## الضبط الدقيق لـ LLAMA
8
+ ### 1. إعداد مجموعة البيانات
9
+
10
+ ```
11
+ .
12
+ ├── SPK1
13
+ │ ├── 21.15-26.44.lab
14
+ │ ├── 21.15-26.44.mp3
15
+ │ ├── 27.51-29.98.lab
16
+ │ ├── 27.51-29.98.mp3
17
+ │ ├── 30.1-32.71.lab
18
+ │ └── 30.1-32.71.mp3
19
+ └── SPK2
20
+ ├── 38.79-40.85.lab
21
+ └── 38.79-40.85.mp3
22
+ ```
23
+
24
+ تحتاج إلى تحويل مجموعة البيانات الخاصة بك إلى التنسيق أعلاه ووضعها تحت مجلد `data`. يمكن أن يكون للملف الصوتي الامتدادات `.mp3`، `.wav`، أو `.flac`، ويجب أن يكون لملف التعليقات التوضيحية الامتداد `.lab`.
25
+
26
+ !!! info "تنسيق مجموعة البيانات"
27
+ يحتاج ملف التعليقات التوضيحية `.lab` فقط إلى احتواء النص المكتوب للمقطع الصوتي، دون الحاجة إلى تنسيق خاص. على سبيل المثال، إذا كان محتوى `hi.mp3` هو "مرحبًا، وداعًا"، فسيحتوي ملف `hi.lab` على سطر واحد من النص: "مرحبًا، وداعًا".
28
+
29
+ !!! warning "تحذير"
30
+ يوصى بتطبيق تسوية جهارة الصوت (loudness normalization) على مجموعة البيانات. يمكنك استخدام [fish-audio-preprocess](https://github.com/fishaudio/audio-preprocess) للقيام بذلك.
31
+ ```bash
32
+ fap loudness-norm data-raw data --clean
33
+ ```
34
+
35
+ ### 2. الاستخراج الدفعي للرموز الدلالية (semantic tokens)
36
+
37
+ تأكد من أنك قمت بتنزيل أوزان VQGAN. إذا لم تكن قد فعلت، قم بتشغيل الأمر التالي:
38
+
39
+ ```bash
40
+ huggingface-cli download fishaudio/openaudio-s1-mini --local-dir checkpoints/openaudio-s1-mini
41
+ ```
42
+
43
+ يمكنك بعد ذلك تشغيل الأمر التالي لاستخراج الرموز الدلالية:
44
+
45
+ ```bash
46
+ python tools/vqgan/extract_vq.py data \
47
+ --num-workers 1 --batch-size 16 \
48
+ --config-name "modded_dac_vq" \
49
+ --checkpoint-path "checkpoints/openaudio-s1-mini/codec.pth"
50
+ ```
51
+
52
+ !!! note "ملاحظة"
53
+ يمكنك ضبط `--num-workers` و `--batch-size` لزيادة سرعة الاستخراج، ولكن يرجى التأكد من عدم تجاوز حد ذاكرة وحدة معالجة الرسومات (GPU) الخاصة بك.
54
+
55
+ سيقوم هذا الأمر بإنشاء ملفات `.npy` في مجلد `data`، كما هو موضح أدناه:
56
+
57
+ ```
58
+ .
59
+ ├── SPK1
60
+ │ ├── 21.15-26.44.lab
61
+ │ ├── 21.15-26.44.mp3
62
+ │ ├── 21.15-26.44.npy
63
+ │ ├── 27.51-29.98.lab
64
+ │ ├── 27.51-29.98.mp3
65
+ │ ├── 27.51-29.98.npy
66
+ │ ├── 30.1-32.71.lab
67
+ │ ├── 30.1-32.71.mp3
68
+ │ └── 30.1-32.71.npy
69
+ └── SPK2
70
+ ├── 38.79-40.85.lab
71
+ ├── 38.79-40.85.mp3
72
+ └── 38.79-40.85.npy
73
+ ```
74
+
75
+ ### 3. حزم مجموعة البيانات في protobuf
76
+
77
+ ```bash
78
+ python tools/llama/build_dataset.py \
79
+ --input "data" \
80
+ --output "data/protos" \
81
+ --text-extension .lab \
82
+ --num-workers 16
83
+ ```
84
+
85
+ بعد انتهاء تنفيذ الأمر، يجب أن ترى ملف `protos` في مجلد `data`.
86
+
87
+ ### 4. أخيرًا، الضبط الدقيق باستخدام LoRA
88
+
89
+ بالمثل، تأكد من أنك قمت بتنزيل أوزان `LLAMA`. إذا لم تكن قد فعلت، قم بتشغيل الأمر التالي:
90
+
91
+ ```bash
92
+ huggingface-cli download fishaudio/openaudio-s1-mini --local-dir checkpoints/openaudio-s1-mini
93
+ ```
94
+
95
+ أخيرًا، يمكنك بدء الضبط الدقيق عن طريق تشغيل الأمر التالي:
96
+
97
+ ```bash
98
+ python fish_speech/train.py --config-name text2semantic_finetune \
99
+ project=$project \
100
+ +lora@model.model.lora_config=r_8_alpha_16
101
+ ```
102
+
103
+ !!! note "ملاحظة"
104
+ يمكنك تعديل معلمات التدريب مثل `batch_size`، `gradient_accumulation_steps`، وما إلى ذلك لتناسب ذاكرة وحدة معالجة الرسومات الخاصة بك عن طريق تعديل `fish_speech/configs/text2semantic_finetune.yaml`.
105
+
106
+ !!! note "ملاحظة"
107
+ لمستخدمي Windows، يمكنك استخدام `trainer.strategy.process_group_backend=gloo` لتجنب مشكلات `nccl`.
108
+
109
+ بعد اكتمال التدريب، يمكنك الرجوع إلى قسم [الاستدلال (inference)](inference.md) لاختبار نموذجك.
110
+
111
+ !!! info "معلومات"
112
+ بشكل افتراضي، سيتعلم النموذج فقط أنماط كلام المتحدث وليس جرس الصوت (timbre). لا تزال بحاجة إلى استخدام التلقينات (prompts) لضمان استقرار جرس الصوت.
113
+ إذا كنت ترغب في تعلم جرس الصوت، يمكنك زيادة عدد خطوات التدريب، ولكن هذا قد يؤدي إلى الإفراط في التخصيص (overfitting).
114
+
115
+ بعد التدريب، تحتاج إلى تحويل أوزان LoRA إلى أوزان عادية قبل إجراء الاستدلال.
116
+
117
+ ```bash
118
+ python tools/llama/merge_lora.py \
119
+ --lora-config r_8_alpha_16 \
120
+ --base-weight checkpoints/openaudio-s1-mini \
121
+ --lora-weight results/$project/checkpoints/step_000000010.ckpt \
122
+ --output checkpoints/openaudio-s1-mini-yth-lora/
123
+ ```
124
+ !!! note "ملاحظة"
125
+ يمكنك أيضًا تجربة نقاط تحقق (checkpoints) أخرى. نقترح استخدام أقدم نقطة تحقق تلبي متطلباتك، حيث إنها غالبًا ما تؤدي أداءً أفضل على البيانات خارج التوزيع (OOD).
vendor/fish-speech/docs/ar/index.md ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+ <h1>Fish Speech</h1>
3
+
4
+ <p><a href="../en/">English</a> | <a href="../zh/">简体中文</a> | <a href="../pt/">Portuguese</a> | <a href="../ja/">日本語</a> | <a href="../ko/">한국어</a> | <strong>العربية</strong></p>
5
+
6
+ <a href="https://www.producthunt.com/products/fish-speech?embed=true&utm_source=badge-top-post-badge&utm_medium=badge&utm_source=badge-fish&#0045;audio&#0045;s1" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=1023740&theme=light&period=daily&t=1761164814710" alt="Fish&#0032;Audio&#0032;S1 - Expressive&#0032;Voice&#0032;Cloning&#0032;and&#0032;Text&#0045;to&#0045;Speech | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
7
+ <a href="https://trendshift.io/repositories/7014" target="_blank">
8
+ <img src="https://trendshift.io/api/badge/repositories/7014" alt="fishaudio%2Ffish-speech | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
9
+ </a>
10
+ </div>
11
+
12
+ <br>
13
+
14
+ <div align="center">
15
+ <img src="https://count.getloli.com/get/@fish-speech?theme=asoul" /><br>
16
+ </div>
17
+
18
+ <br>
19
+
20
+ <div align="center">
21
+ <a target="_blank" href="https://discord.gg/Es5qTB9BcN">
22
+ <img alt="Discord" src="https://img.shields.io/discord/1214047546020728892?color=%23738ADB&label=Discord&logo=discord&logoColor=white&style=flat-square"/>
23
+ </a>
24
+ <a target="_blank" href="https://hub.docker.com/r/fishaudio/fish-speech">
25
+ <img alt="Docker" src="https://img.shields.io/docker/pulls/fishaudio/fish-speech?style=flat-square&logo=docker"/>
26
+ </a>
27
+ <a target="_blank" href="https://pd.qq.com/s/bwxia254o">
28
+ <img alt="QQ Channel" src="https://img.shields.io/badge/QQ-blue?logo=tencentqq">
29
+ </a>
30
+ </div>
31
+
32
+ <div align="center">
33
+ <a target="_blank" href="https://huggingface.co/fishaudio/s2">
34
+ <img alt="HuggingFace Model" src="https://img.shields.io/badge/🤗%20-models-orange"/>
35
+ </a>
36
+ <a target="_blank" href="https://fish.audio/blog/fish-audio-open-sources-s2/">
37
+ <img alt="Fish Audio Blog" src="https://img.shields.io/badge/Blog-Fish_Audio_S2-1f7a8c?style=flat-square&logo=readme&logoColor=white"/>
38
+ </a>
39
+ <a target="_blank" href="https://arxiv.org/abs/2603.08823">
40
+ <img alt="Paper | Technical Report" src="https://img.shields.io/badge/Paper-Technical_Report-b31b1b?style=flat-square"/>
41
+ </a>
42
+ </div>
43
+
44
+ !!! info "تنبيه الترخيص"
45
+ يتم إصدار قاعدة الأكواد هذه وأوزان النماذج المرتبطة بها بموجب رخصة **FISH AUDIO RESEARCH LICENSE**. يرجى الرجوع إلى [LICENSE](https://github.com/fishaudio/fish-speech/blob/main/LICENSE) لمزيد من التفاصيل.
46
+
47
+ !!! warning "إخلاء المسؤولية القانونية"
48
+ نحن لا نتحمل أي مسؤولية عن أي استخدام غير قانوني لقاعدة الأكواد. يرجى مراجعة القوانين المحلية المتعلقة بـ DMCA والقوانين الأخرى ذات الصلة.
49
+
50
+ ## البدء السريع
51
+
52
+ ### ابدأ من الوثائق
53
+
54
+ هذه هي الوثائق الرسمية لـ Fish Audio S2، ويمكنك البدء مباشرة عبر الروابط التالية:
55
+
56
+ - [التثبيت](https://speech.fish.audio/ar/install/)
57
+ - [الاستدلال عبر سطر الأوامر](https://speech.fish.audio/ar/inference/)
58
+ - [استدلال WebUI](https://speech.fish.audio/ar/inference/)
59
+ - [الاستدلال عبر الخادم](https://speech.fish.audio/ar/server/)
60
+ - [إعداد Docker](https://speech.fish.audio/ar/install/)
61
+
62
+ > [!IMPORTANT]
63
+ > **بالنسبة لخادم SGLang، راجع [SGLang-Omni README](https://github.com/sgl-project/sglang-omni/blob/main/sglang_omni/models/fishaudio_s2_pro/README.md).**
64
+
65
+ ### دليل وكلاء LLM
66
+
67
+ ```
68
+ قم بتثبيت وإعداد Fish Audio S2 باتباع التعليمات في https://speech.fish.audio/ar/install/ .
69
+ ```
70
+
71
+ ## Fish Audio S2
72
+ **أفضل نظام لتحويل النص إلى كلام بين الأنظمة مفتوحة المصدر ومغلقة المصدر**
73
+
74
+ Fish Audio S2 هو أحدث نموذج من [Fish Audio](https://fish.audio/). تم تدريبه على أكثر من 10 ملايين ساعة صوتية عبر نحو 50 لغة، ويجمع بين المواءمة بالتعلم المعزز وبنية Dual-Autoregressive لإنتاج كلام طبيعي وواقعي وغني بالتعبير العاطفي.
75
+
76
+ يدعم S2 التحكم الدقيق في النبرة والعاطفة داخل النص نفسه باستخدام وسوم باللغة الطبيعية مثل `[laugh]` و`[whispers]` و`[super happy]`، كما يدعم بشكل أصيل توليد متحدثين متعددين وحوارات متعددة الأدوار.
77
+
78
+ يمكنك تجربة النموذج مباشرة عبر [موقع Fish Audio](https://fish.audio/)، وقراءة المزيد في [منشور المدونة](https://fish.audio/blog/fish-audio-open-sources-s2/) و[التقرير التقني](https://arxiv.org/abs/2603.08823).
79
+
80
+ ### إصدارات النموذج
81
+
82
+ | النموذج | الحجم | التوفر | الوصف |
83
+ |------|------|-------------|-------------|
84
+ | S2-Pro | 4B معلمة | [HuggingFace](https://huggingface.co/fishaudio/s2-pro) | نموذج رائد كامل الميزات بأعلى مستوى من الجودة والاستقرار |
85
+
86
+ يمكن العثور على مزيد من التفاصيل في [التقرير التقني](https://arxiv.org/abs/2411.01156).
87
+
88
+ ## نتائج القياس المعياري
89
+
90
+ | المعيار | Fish Audio S2 |
91
+ |------|------|
92
+ | Seed-TTS Eval — WER (الصينية) | **0.54%** (الأفضل إجمالاً) |
93
+ | Seed-TTS Eval — WER (الإنجليزية) | **0.99%** (الأفضل إجمالاً) |
94
+ | Audio Turing Test (مع التعليمات) | **0.515** المتوسط البعدي |
95
+ | EmergentTTS-Eval — معدل الفوز | **81.88%** (الأعلى إجمالاً) |
96
+ | Fish Instruction Benchmark — TAR | **93.3%** |
97
+ | Fish Instruction Benchmark — الجودة | **4.51 / 5.0** |
98
+ | متعدد اللغات (MiniMax Testset) — أفضل WER | **11 من 24** لغة |
99
+ | متعدد اللغات (MiniMax Testset) — أفضل SIM | **17 من 24** لغة |
100
+
101
+ في Seed-TTS Eval، حقق S2 أقل WER بين جميع النماذج التي تم تقييمها، بما في ذلك الأنظمة المغلقة: Qwen3-TTS ‏(0.77/1.24)، وMiniMax Speech-02 ‏(0.99/1.90)، وSeed-TTS ‏(1.12/2.25). وفي Audio Turing Test، تفوقت قيمة 0.515 على Seed-TTS ‏(0.417) بنسبة 24% وعلى MiniMax-Speech ‏(0.387) بنسبة 33%. وفي EmergentTTS-Eval، حقق S2 نتائج قوية بشكل خاص في الخصائص شبه اللغوية (91.61%)، والأسئلة (84.41%)، والتعقيد النحوي (83.39%).
102
+
103
+ ## أبرز المميزات
104
+
105
+ <img src="../assets/totalability.png" width=200%>
106
+
107
+ ### تحكم مضمّن دقيق عبر اللغة الطبيعية
108
+
109
+ يتيح Fish Audio S2 تحكمًا موضعيًا في توليد الكلام من خلال تضمين تعليمات باللغة الطبيعية مباشرة عند مواقع كلمات أو عبارات محددة داخل النص. وبدلًا من الاعتماد على مجموعة ثابتة من الوسوم المُعرّفة مسبقًا، يقبل S2 أوصافًا نصية حرة مثل [whisper in small voice] أو [professional broadcast tone] أو [pitch up]، مما يتيح تحكمًا مفتوحًا في التعبير على مستوى الكلمة.
110
+
111
+ ### بنية Dual-Autoregressive
112
+
113
+ يعتمد S2 على Transformer أحادي الاتجاه (Decoder-only) مع مُرمّز صوتي قائم على RVQ (عدد 10 codebooks وبمعدل إطارات يقارب 21 هرتز). وتُقسّم بنية Dual-AR عملية التوليد إلى مرحلتين:
114
+
115
+ - **Slow AR** يعمل على المحور الزمني ويتنبأ بالـ semantic codebook الأساسي.
116
+ - **Fast AR** يولّد الـ 9 residual codebooks المتبقية في كل خطوة زمنية لإعادة بناء التفاصيل الصوتية الدقيقة.
117
+
118
+ هذا التصميم غير المتماثل (4B معلمة على المحور الزمني و400M على محور العمق) يرفع كفاءة الاستدلال مع الحفاظ على جودة الصوت.
119
+
120
+ ### المواءمة بالتعلم المعزز
121
+
122
+ يستخدم S2 خوارزمية Group Relative Policy Optimization (GRPO) للمواءمة بعد التدريب. ويتم إعادة استخدام نفس النماذج التي استُخدمت لتصفية بيانات التدريب وتعليقها كنماذج مكافأة في التعلم المعزز مباشرة، مما يلغي عدم تطابق التوزيع بين بيانات ما قبل التدريب وأهداف ما بعد التدريب. وتجمع إشارة المكافأة بين الدقة الدلالية، والالتزام بالتعليمات، وتقييم التفضيل الصوتي، وتشابه النبرة.
123
+
124
+ ### البث الإنتاجي عبر SGLang
125
+
126
+ لأن بنية Dual-AR متماثلة بنيويًا مع نماذج LLM autoregressive القياسية، فإن S2 يرث مباشرة تحسينات الخدمة الأصلية في SGLang، بما في ذلك: continuous batching، وpaged KV cache، وCUDA graph replay، وprefix caching المعتمد على RadixAttention.
127
+
128
+ على بطاقة NVIDIA H200 واحدة:
129
+
130
+ - **عامل الزمن الحقيقي (RTF):** 0.195
131
+ - **الزمن حتى أول مقطع صوتي:** حوالي 100 مللي ثانية
132
+ - **معدل المعالجة:** أكثر من 3,000 acoustic tokens/s مع الحفاظ على RTF أقل من 0.5
133
+
134
+ ### دعم لغات متعددة
135
+
136
+ يدعم Fish Audio S2 تحويل النص إلى كلام بجودة عالية ولغات متعددة دون الحاجة إلى رموز صوتية أو معالجة مسبقة خاصة بكل لغة. بما في ذلك:
137
+
138
+ **الإنجليزية، الصينية، اليابانية، الكورية، العربية، الألمانية، الفرنسية...**
139
+
140
+ **وأكثر من ذلك بكثير!**
141
+
142
+ القائمة في توسع مستمر، تحقق من [Fish Audio](https://fish.audio/) لمعرفة أحدث الإصدارات.
143
+
144
+ ### توليد أصلي لمتحدثين متعددين
145
+
146
+ <img src="../assets/chattemplate.png" width=200%>
147
+
148
+ يسمح Fish Audio S2 للمستخدمين برفع صوت مرجعي يحتوي على متحدثين متعددين، وسيتعامل النموذج مع ميزات كل متحدث عبر رمز `<|speaker:i|>`. يمكنك بعد ذلك التحكم في أداء النموذج باستخدام رمز معرف المتحدث، مما يسمح بتوليد واحد يتضمن متحدثين متعددين. لم تعد بحاجة لرفع ملفات مرجعية منفصلة لكل متحدث.
149
+
150
+ ### توليد حوارات متعددة الأدوار
151
+
152
+ بفضل توسيع سياق النموذج، يمكن لنموذجنا الآن استخدام المعلومات السابقة لتحسين التعبير في المحتوى المولد لاحقاً، مما يزيد من طبيعية المحتوى.
153
+
154
+ ### استنساخ صوت سريع
155
+
156
+ يدعم Fish Audio S2 استنساخ الصوت بدقة باستخدام عينة مرجعية قصيرة (عادةً 10-30 ثانية). يلتقط النموذج نبرة الصوت، وأسلوب التحدث، والميول العاطفية، مما ينتج أصواتاً مستنسخة واقعية ومتسقة دون الحاجة إلى ضبط دقيق إضافي.
157
+ لاستخدام خادم SGLang، راجع [SGLang-Omni README](https://github.com/sgl-project/sglang-omni/blob/main/sglang_omni/models/fishaudio_s2_pro/README.md) .
158
+
159
+ ---
160
+
161
+ ## شكر وتقدير
162
+
163
+ - [VITS2 (daniilrobnikov)](https://github.com/daniilrobnikov/vits2)
164
+ - [Bert-VITS2](https://github.com/fishaudio/Bert-VITS2)
165
+ - [GPT VITS](https://github.com/innnky/gpt-vits)
166
+ - [MQTTS](https://github.com/b04901014/MQTTS)
167
+ - [GPT Fast](https://github.com/pytorch-labs/gpt-fast)
168
+ - [GPT-SoVITS](https://github.com/RVC-Boss/GPT-SoVITS)
169
+ - [Qwen3](https://github.com/QwenLM/Qwen3)
170
+
171
+ ## التقرير التقني
172
+ ```bibtex
173
+ @misc{fish-speech-v1.4,
174
+ title={Fish-Speech: Leveraging Large Language Models for Advanced Multilingual Text-to-Speech Synthesis},
175
+ author={Shijia Liao and Yuxuan Wang and Tianyu Li and Yifan Cheng and Ruoyi Zhang and Rongzhi Zhou and Yijin Xing},
176
+ year={2024},
177
+ eprint={2411.01156},
178
+ archivePrefix={arXiv},
179
+ primaryClass={cs.SD},
180
+ url={https://arxiv.org/abs/2411.01156},
181
+ }
182
+
183
+ @misc{liao2026fishaudios2technical,
184
+ title={Fish Audio S2 Technical Report},
185
+ author={Shijia Liao and Yuxuan Wang and Songting Liu and Yifan Cheng and Ruoyi Zhang and Tianyu Li and Shidong Li and Yisheng Zheng and Xingwei Liu and Qingzheng Wang and Zhizhuo Zhou and Jiahua Liu and Xin Chen and Dawei Han},
186
+ year={2026},
187
+ eprint={2603.08823},
188
+ archivePrefix={arXiv},
189
+ primaryClass={cs.SD},
190
+ url={https://arxiv.org/abs/2603.08823},
191
+ }
192
+ ```
vendor/fish-speech/docs/ar/inference.md ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # الاستنتاج
2
+
3
+ يتطلب نموذج Fish Audio S2 ذاكرة فيديو (VRAM) كبيرة. نوصي باستخدام وحدة معالجة رسومات (GPU) بسعة 24 جيجابايت على الأقل للاستنتاج.
4
+
5
+ ## تحميل الأوزان
6
+
7
+ أولاً ، تحتاج إلى تحميل أوزان النموذج:
8
+
9
+ ```bash
10
+ hf download fishaudio/s2-pro --local-dir checkpoints/s2-pro
11
+ ```
12
+
13
+ ## الاستنتاج عبر خط الأوامر
14
+
15
+ !!! note
16
+ إذا كنت تخطط لترك النموذج يختار نغمة الصوت عشوائيًا ، فيمكنك تخطي هذه الخطوة.
17
+
18
+ ### 1. الحصول على رموز VQ من الصوت المرجعي
19
+
20
+ ```bash
21
+ python fish_speech/models/dac/inference.py \
22
+ -i "test.wav" \
23
+ --checkpoint-path "checkpoints/s2-pro/codec.pth"
24
+ ```
25
+
26
+ يجب أن تحصل على `fake.npy` و `fake.wav`.
27
+
28
+ ### 2. توليد الرموز الدلالية (Semantic tokens) من النص:
29
+
30
+ ```bash
31
+ python fish_speech/models/text2semantic/inference.py \
32
+ --text "النص الذي تريد تحويله" \
33
+ --prompt-text "النص المرجعي الخاص بك" \
34
+ --prompt-tokens "fake.npy" \
35
+ # --compile
36
+ ```
37
+
38
+ سيقوم هذا الأمر بإنشاء ملف `codes_N` في دليل العمل ، حيث N هو عدد صحيح يبدأ من 0.
39
+
40
+ !!! note
41
+ قد ترغب في استخدام `--compile` لدمج نوى CUDA لاستنتاج أسرع. ومع ذلك ، نوصي باستخدام تحسين تسريع الاستنتاج sglang الخاص بنا.
42
+ بالمقابل ، إذا كنت لا تخطط لاستخدام التسريع ، يمكنك التعليق على معلمة `--compile`.
43
+
44
+ !!! info
45
+ بالنسبة لوحدات معالجة الرسومات التي لا تدعم bf16 ، قد تحتاج إلى استخدام معلمة `--half`.
46
+
47
+ ### 3. توليد الصوت من الرموز الدلالية:
48
+
49
+ ```bash
50
+ python fish_speech/models/dac/inference.py \
51
+ -i "codes_0.npy" \
52
+ ```
53
+
54
+ بعد ذلك ستحصل على ملف `fake.wav`.
55
+
56
+ ## استنتاج WebUI
57
+
58
+ ### 1. Gradio WebUI
59
+
60
+ للحفاظ على التوافق، ما زلنا نحتفظ بواجهة Gradio WebUI السابقة.
61
+
62
+ ```bash
63
+ python tools/run_webui.py # --compile إذا كنت بحاجة إلى تسريع
64
+ ```
65
+
66
+ ### 2. Awesome WebUI
67
+
68
+ تعد Awesome WebUI واجهة ويب حديثة تعتمد على TypeScript، وتوفر ميزات أغنى وتجربة مستخدم أفضل.
69
+
70
+ **بناء WebUI:**
71
+
72
+ يجب أن يكون لديك Node.js و npm مثبتين على جهازك المحلي أو الخادم.
73
+
74
+ 1. ادخل إلى دليل `awesome_webui`:
75
+ ```bash
76
+ cd awesome_webui
77
+ ```
78
+ 2. تثبيت التبعيات:
79
+ ```bash
80
+ npm install
81
+ ```
82
+ 3. بناء WebUI:
83
+ ```bash
84
+ npm run build
85
+ ```
86
+
87
+ **بدء تشغيل خادم الخلفية:**
88
+
89
+ بعد بناء WebUI، عد إلى دليل جذر المشروع وقم بتشغيل خادم API:
90
+
91
+ ```bash
92
+ python tools/api_server.py --listen 0.0.0.0:8888 --compile
93
+ ```
94
+
95
+ **الوصول:**
96
+
97
+ بمجرد تشغيل الخادم، يمكنك الوصول إليه عبر المتصفح على العنوان التالي:
98
+ `http://localhost:8888/ui`
vendor/fish-speech/docs/ar/install.md ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## المتطلبات
2
+
3
+ - ذاكرة وحدة معالجة الرسومات (GPU): 24 جيجابايت (للاستدلال)
4
+ - النظام: Linux, WSL
5
+
6
+ ## إعداد النظام
7
+
8
+ يدعم Fish Audio S2 طرق تثبيت متعددة. اختر الطريقة التي تناسب بيئة التطوير الخاصة بك.
9
+
10
+ **المتطلبات الأساسية**: قم بتثبيت تبعيات النظام لمعالجة الصوت:
11
+ ``` bash
12
+ apt install portaudio19-dev libsox-dev ffmpeg
13
+ ```
14
+
15
+ ### Conda
16
+
17
+ ```bash
18
+ conda create -n fish-speech python=3.12
19
+ conda activate fish-speech
20
+
21
+ # تثبيت نسخة GPU (اختر إصدار CUDA الخاص بك: cu126, cu128, cu129)
22
+ pip install -e .[cu129]
23
+
24
+ # تثبيت نسخة CPU فقط
25
+ pip install -e .[cpu]
26
+
27
+ # التثبيت الافتراضي (يستخدم فهرس PyTorch الافتراضي)
28
+ pip install -e .
29
+
30
+ # إذا واجهت خطأ أثناء التثبيت بسبب pyaudio، ففكر في استخدام الأمر التالي:
31
+ # conda install pyaudio
32
+ # ثم قم بتشغيل pip install -e . مرة أخرى
33
+ ```
34
+
35
+ ### UV
36
+
37
+ يوفر UV حلاً أسرع لتثبيت التبعيات:
38
+
39
+ ```bash
40
+ # تثبيت نسخة GPU (اختر إصدار CUDA الخاص بك: cu126, cu128, cu129)
41
+ uv sync --python 3.12 --extra cu129
42
+
43
+ # تثبيت نسخة CPU فقط
44
+ uv sync --python 3.12 --extra cpu
45
+ ```
46
+ ### دعم Intel Arc XPU
47
+
48
+ لمستخدمي وحدات معالجة الرسومات Intel Arc، قم بالتثبيت مع دعم XPU على النحو التالي:
49
+
50
+ ```bash
51
+ conda create -n fish-speech python=3.12
52
+ conda activate fish-speech
53
+
54
+ # تثبيت مكتبة C++ القياسية المطلوبة
55
+ conda install libstdcxx -c conda-forge
56
+
57
+ # تثبيت PyTorch مع دعم Intel XPU
58
+ pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/xpu
59
+
60
+ # تثبيت Fish Speech
61
+ pip install -e .
62
+ ```
63
+
64
+ !!! warning
65
+ خيار `compile` غير مدعوم على أنظمة Windows و macOS. إذا كنت ترغب في التشغيل مع التجميع، ستحتاج إلى تثبيت Triton بنفسك.
66
+
67
+
68
+ ## إعداد Docker
69
+
70
+ يوفر نموذج سلسلة Fish Audio S2 خيارات نشر متعددة مع Docker لتلبية الاحتياجات المختلفة. يمكنك استخدام الصور المعدة مسبقًا من Docker Hub، أو البناء محليًا باستخدام Docker Compose، أو بناء صور مخصصة يدويًا.
71
+
72
+ لقد قدمنا صور Docker لكل من واجهة المستخدم الرسومية (WebUI) وخادم API، لكل من وحدات معالجة الرسومات (GPU) (CUDA 12.6 افتراضيًا) ووحدات المعالجة المركزية (CPU). يمكنك استخدام الصور المعدة مسبقًا من Docker Hub، أو البناء محليًا باستخدام Docker Compose، أو بناء صور مخصصة يدويًا. إذا كنت ترغب في البناء محليًا، فاتبع الإرشادات أدناه. إذا كنت ترغب فقط في استخدام الصور المعدة مسبقًا، فاتبع مباشرةً [دليل الاستدلال](inference.md).
73
+
74
+ ### المتطلبات الأساسية
75
+
76
+ - تثبيت Docker و Docker Compose
77
+ - تثبيت NVIDIA Docker runtime (لدعم GPU)
78
+ - ذاكرة GPU لا تقل عن 24 جيجابايت للاستدلال باستخدام CUDA
79
+
80
+ ### استخدام Docker Compose
81
+
82
+ للتطوير أو التخصيص، يمكنك استخدام Docker Compose للبناء والتشغيل محليًا:
83
+
84
+ ```bash
85
+ # أولاً، استنسخ المستودع
86
+ git clone https://github.com/fishaudio/fish-speech.git
87
+ cd fish-speech
88
+
89
+ # بدء واجهة المستخدم الرسومية (WebUI) مع CUDA
90
+ docker compose --profile webui up
91
+
92
+ # بدء واجهة المستخدم الرسومية (WebUI) مع تحسين التجميع
93
+ COMPILE=1 docker compose --profile webui up
94
+
95
+ # بدء خادم API
96
+ docker compose --profile server up
97
+
98
+ # بدء خادم API مع تحسين التجميع
99
+ COMPILE=1 docker compose --profile server up
100
+
101
+ # النشر باستخدام CPU فقط
102
+ BACKEND=cpu docker compose --profile webui up
103
+ ```
104
+
105
+ #### متغيرات البيئة لـ Docker Compose
106
+
107
+ يمكنك تخصيص النشر باستخدام متغيرات البيئة:
108
+
109
+ ```bash
110
+ # مثال على ملف .env
111
+ BACKEND=cuda # أو cpu
112
+ COMPILE=1 # تمكين تحسين التجميع
113
+ GRADIO_PORT=7860 # منفذ واجهة المستخدم الرسومية (WebUI)
114
+ API_PORT=8080 # منفذ خادم API
115
+ UV_VERSION=0.8.15 # إصدار مدير الحزم UV
116
+ ```
117
+
118
+ سيقوم الأمر ببناء الصورة وتشغيل الحاوية. يمكنك الوصول إلى واجهة المستخدم الرسومية (WebUI) على `http://localhost:7860` وخادم API على `http://localhost:8080`.
119
+
120
+ ### البناء اليدوي باستخدام Docker
121
+
122
+ للمستخدمين المتقدمين الذين يرغبون في تخصيص عملية البناء:
123
+
124
+ ```bash
125
+ # بناء صورة واجهة المستخدم الرسومية (WebUI) مع دعم CUDA
126
+ docker build \
127
+ --platform linux/amd64 \
128
+ -f docker/Dockerfile \
129
+ --build-arg BACKEND=cuda \
130
+ --build-arg CUDA_VER=12.6.0 \
131
+ --build-arg UV_EXTRA=cu126 \
132
+ --target webui \
133
+ -t fish-speech-webui:cuda .
134
+
135
+ # بناء صورة خادم API مع دعم CUDA
136
+ docker build \
137
+ --platform linux/amd64 \
138
+ -f docker/Dockerfile \
139
+ --build-arg BACKEND=cuda \
140
+ --build-arg CUDA_VER=12.6.0 \
141
+ --build-arg UV_EXTRA=cu126 \
142
+ --target server \
143
+ -t fish-speech-server:cuda .
144
+
145
+ # بناء صورة CPU فقط (تدعم منصات متعددة)
146
+ docker build \
147
+ --platform linux/amd64,linux/arm64 \
148
+ -f docker/Dockerfile \
149
+ --build-arg BACKEND=cpu \
150
+ --target webui \
151
+ -t fish-speech-webui:cpu .
152
+
153
+ # بناء صورة التطوير
154
+ docker build \
155
+ --platform linux/amd64 \
156
+ -f docker/Dockerfile \
157
+ --build-arg BACKEND=cuda \
158
+ --target dev \
159
+ -t fish-speech-dev:cuda .
160
+ ```
161
+
162
+ #### وسيطات البناء
163
+
164
+ - `BACKEND`: `cuda` أو `cpu` (الافتراضي: `cuda`)
165
+ - `CUDA_VER`: إصدار CUDA (الافتراضي: `12.6.0`)
166
+ - `UV_EXTRA`: حزمة UV إضافية لـ CUDA (الافتراضي: `cu126`)
167
+ - `UBUNTU_VER`: إصدار Ubuntu (الافتراضي: `24.04`)
168
+ - `PY_VER`: إصدار Python (الافتراضي: `3.12`)
169
+
170
+ ### تحميل المجلدات
171
+
172
+ تتطلب كلتا الطريقتين تحميل المجلدات التالية:
173
+
174
+ - `./checkpoints:/app/checkpoints` - مجلد أوزان النموذج
175
+ - `./references:/app/references` - مجلد ملفات الصوت المرجعية
176
+
177
+ ### متغيرات البيئة
178
+
179
+ - `COMPILE=1` - تمكين `torch.compile` لتسريع الاستدلال (حوالي 10 أضعاف)
180
+ - `GRADIO_SERVER_NAME=0.0.0.0` - مضيف خادم واجهة المستخدم الرسومية (WebUI)
181
+ - `GRADIO_SERVER_PORT=7860` - منفذ خادم واجهة المستخدم الرسومية (WebUI)
182
+ - `API_SERVER_NAME=0.0.0.0` - مضيف خادم API
183
+ - `API_SERVER_PORT=8080` - منفذ خادم API
184
+
185
+ !!! note
186
+ تتوقع حاويات Docker أن يتم تحميل أوزان النموذج في `/app/checkpoints`. تأكد من تنزيل أوزان النموذج المطلوبة قبل بدء الحاويات.
187
+
188
+ !!! warning
189
+ يتطلب دعم GPU وجود NVIDIA Docker runtime. للنشر باستخدام CPU فقط، قم بإزالة علامة `--gpus all` واستخدم صور CPU.
vendor/fish-speech/docs/assets/logo.svg ADDED
vendor/fish-speech/docs/en/finetune.md ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Fine-tuning
2
+
3
+ !!! warning
4
+ We highly do note recoomand users to do fine-tuning on an RL trained model. Fine-tuning a model after RL can shift the model distribution, which may lead to degraded performance.
5
+
6
+ In the current version, you only need to finetune the 'LLAMA' part.
7
+
8
+ ## Fine-tuning LLAMA
9
+ ### 1. Prepare the dataset
10
+
11
+ ```
12
+ .
13
+ ├── SPK1
14
+ │ ├── 21.15-26.44.lab
15
+ │ ├── 21.15-26.44.mp3
16
+ │ ├── 27.51-29.98.lab
17
+ │ ├── 27.51-29.98.mp3
18
+ │ ├── 30.1-32.71.lab
19
+ │ └── 30.1-32.71.mp3
20
+ └── SPK2
21
+ ├── 38.79-40.85.lab
22
+ └── 38.79-40.85.mp3
23
+ ```
24
+
25
+ You need to convert your dataset into the above format and place it under `data`. The audio file can have the extensions `.mp3`, `.wav`, or `.flac`, and the annotation file should have the extension `.lab`.
26
+
27
+ !!! info
28
+ The `.lab` annotation file only needs to contain the transcription of the audio, with no special formatting required. For example, if `hi.mp3` says "Hello, goodbye," then the `hi.lab` file would contain a single line of text: "Hello, goodbye."
29
+
30
+ !!! warning
31
+ It's recommended to apply loudness normalization to the dataset. You can use [fish-audio-preprocess](https://github.com/fishaudio/audio-preprocess) to do this.
32
+
33
+ ```bash
34
+ fap loudness-norm data-raw data --clean
35
+ ```
36
+
37
+
38
+ ### 2. Batch extraction of semantic tokens
39
+
40
+ Make sure you have downloaded the VQGAN weights. If not, run the following command:
41
+
42
+ ```bash
43
+ huggingface-cli download fishaudio/openaudio-s1-mini --local-dir checkpoints/openaudio-s1-mini
44
+ ```
45
+
46
+ You can then run the following command to extract semantic tokens:
47
+
48
+ ```bash
49
+ python tools/vqgan/extract_vq.py data \
50
+ --num-workers 1 --batch-size 16 \
51
+ --config-name "modded_dac_vq" \
52
+ --checkpoint-path "checkpoints/openaudio-s1-mini/codec.pth"
53
+ ```
54
+
55
+ !!! note
56
+ You can adjust `--num-workers` and `--batch-size` to increase extraction speed, but please make sure not to exceed your GPU memory limit.
57
+
58
+ This command will create `.npy` files in the `data` directory, as shown below:
59
+
60
+ ```
61
+ .
62
+ ├── SPK1
63
+ │ ├── 21.15-26.44.lab
64
+ │ ├── 21.15-26.44.mp3
65
+ │ ├── 21.15-26.44.npy
66
+ │ ├── 27.51-29.98.lab
67
+ │ ├── 27.51-29.98.mp3
68
+ │ ├── 27.51-29.98.npy
69
+ │ ├── 30.1-32.71.lab
70
+ │ ├── 30.1-32.71.mp3
71
+ │ └── 30.1-32.71.npy
72
+ └── SPK2
73
+ ├── 38.79-40.85.lab
74
+ ├── 38.79-40.85.mp3
75
+ └── 38.79-40.85.npy
76
+ ```
77
+
78
+ ### 3. Pack the dataset into protobuf
79
+
80
+ ```bash
81
+ python tools/llama/build_dataset.py \
82
+ --input "data" \
83
+ --output "data/protos" \
84
+ --text-extension .lab \
85
+ --num-workers 16
86
+ ```
87
+
88
+ After the command finishes executing, you should see the `protos` file in the `data` directory.
89
+
90
+ ### 4. Finally, fine-tuning with LoRA
91
+
92
+ Similarly, make sure you have downloaded the `LLAMA` weights. If not, run the following command:
93
+
94
+ ```bash
95
+ huggingface-cli download fishaudio/openaudio-s1-mini --local-dir checkpoints/openaudio-s1-mini
96
+ ```
97
+
98
+ Finally, you can start the fine-tuning by running the following command:
99
+
100
+ ```bash
101
+ python fish_speech/train.py --config-name text2semantic_finetune \
102
+ project=$project \
103
+ +lora@model.model.lora_config=r_8_alpha_16
104
+ ```
105
+
106
+ !!! note
107
+ You can modify the training parameters such as `batch_size`, `gradient_accumulation_steps`, etc. to fit your GPU memory by modifying `fish_speech/configs/text2semantic_finetune.yaml`.
108
+
109
+ !!! note
110
+ For Windows users, you can use `trainer.strategy.process_group_backend=gloo` to avoid `nccl` issues.
111
+
112
+ After training is complete, you can refer to the [inference](inference.md) section to test your model.
113
+
114
+ !!! info
115
+ By default, the model will only learn the speaker's speech patterns and not the timbre. You still need to use prompts to ensure timbre stability.
116
+ If you want to learn the timbre, you can increase the number of training steps, but this may lead to overfitting.
117
+
118
+ After training, you need to convert the LoRA weights to regular weights before performing inference.
119
+
120
+ ```bash
121
+ python tools/llama/merge_lora.py \
122
+ --lora-config r_8_alpha_16 \
123
+ --base-weight checkpoints/openaudio-s1-mini \
124
+ --lora-weight results/$project/checkpoints/step_000000010.ckpt \
125
+ --output checkpoints/openaudio-s1-mini-yth-lora/
126
+ ```
127
+ !!! note
128
+ You may also try other checkpoints. We suggest using the earliest checkpoint that meets your requirements, as they often perform better on out-of-distribution (OOD) data.
vendor/fish-speech/docs/en/index.md ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+ <h1>Fish Speech</h1>
3
+
4
+ <p><strong>English</strong> | <a href="../zh/">简体中文</a> | <a href="../pt/">Portuguese</a> | <a href="../ja/">日本語</a> | <a href="../ko/">한국어</a> | <a href="../ar/">العربية</a> | <a href="../es/">Español</a></p>
5
+
6
+ <a href="https://www.producthunt.com/products/fish-speech?embed=true&utm_source=badge-top-post-badge&utm_medium=badge&utm_source=badge-fish&#0045;audio&#0045;s1" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=1023740&theme=light&period=daily&t=1761164814710" alt="Fish&#0032;Audio&#0032;S1 - Expressive&#0032;Voice&#0032;Cloning&#0032;and&#0032;Text&#0045;to&#0045;Speech | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
7
+ <a href="https://trendshift.io/repositories/7014" target="_blank">
8
+ <img src="https://trendshift.io/api/badge/repositories/7014" alt="fishaudio%2Ffish-speech | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
9
+ </a>
10
+ </div>
11
+
12
+ <br>
13
+
14
+ <div align="center">
15
+ <img src="https://count.getloli.com/get/@fish-speech?theme=asoul" /><br>
16
+ </div>
17
+
18
+ <br>
19
+
20
+ <div align="center">
21
+ <a target="_blank" href="https://discord.gg/Es5qTB9BcN">
22
+ <img alt="Discord" src="https://img.shields.io/discord/1214047546020728892?color=%23738ADB&label=Discord&logo=discord&logoColor=white&style=flat-square"/>
23
+ </a>
24
+ <a target="_blank" href="https://hub.docker.com/r/fishaudio/fish-speech">
25
+ <img alt="Docker" src="https://img.shields.io/docker/pulls/fishaudio/fish-speech?style=flat-square&logo=docker"/>
26
+ </a>
27
+ <a target="_blank" href="https://pd.qq.com/s/bwxia254o">
28
+ <img alt="QQ Channel" src="https://img.shields.io/badge/QQ-blue?logo=tencentqq">
29
+ </a>
30
+ </div>
31
+
32
+ <div align="center">
33
+ <a target="_blank" href="https://huggingface.co/fishaudio/s2">
34
+ <img alt="HuggingFace Model" src="https://img.shields.io/badge/🤗%20-models-orange"/>
35
+ </a>
36
+ <a target="_blank" href="https://fish.audio/blog/fish-audio-open-sources-s2/">
37
+ <img alt="Fish Audio Blog" src="https://img.shields.io/badge/Blog-Fish_Audio_S2-1f7a8c?style=flat-square&logo=readme&logoColor=white"/>
38
+ </a>
39
+ <a target="_blank" href="https://arxiv.org/abs/2603.08823">
40
+ <img alt="Paper | Technical Report" src="https://img.shields.io/badge/Paper-Technical_Report-b31b1b?style=flat-square"/>
41
+ </a>
42
+ </div>
43
+
44
+ !!! info "License Notice"
45
+ This codebase and its associated model weights are released under **FISH AUDIO RESEARCH LICENSE**. Please refer to [LICENSE](https://github.com/fishaudio/fish-speech/blob/main/LICENSE) for more details. We will take action against any violation of the license.
46
+
47
+ !!! warning "Legal Disclaimer"
48
+ We do not hold any responsibility for any illegal usage of the codebase. Please refer to your local laws about DMCA and other related laws.
49
+
50
+ ## Quick Start
51
+
52
+ ### For Human
53
+
54
+ Here are the official documents for Fish Audio S2, follow the instructions to get started easily.
55
+
56
+ - [Installation](https://speech.fish.audio/install/)
57
+ - [Command Line Inference](https://speech.fish.audio/inference/#command-line-inference)
58
+ - [WebUI Inference](https://speech.fish.audio/inference/#webui-inference)
59
+ - [Server Inference](https://speech.fish.audio/server/)
60
+ - [Docker Setup](https://speech.fish.audio/install/#docker-setup)
61
+
62
+ > [!IMPORTANT]
63
+ > **For SGLang server, please read [SGLang-Omni README](https://github.com/sgl-project/sglang-omni/blob/main/sglang_omni/models/fishaudio_s2_pro/README.md).**
64
+
65
+ ### For LLM Agent
66
+
67
+ ```
68
+ Install and configure Fish-Audio S2 by following the instructions here: https://speech.fish.audio/install/
69
+ ```
70
+
71
+ ## Fish Audio S2
72
+ **Best text-to-speech system among both open source and closed source**
73
+
74
+ Fish Audio S2 is the latest model developed by [Fish Audio](https://fish.audio/). Trained on over 10 million hours of audio across approximately 50 languages, S2 combines reinforcement learning alignment with a Dual-Autoregressive architecture to generate speech that sounds natural, realistic, and emotionally rich.
75
+
76
+ S2 supports fine-grained inline control of prosody and emotion using natural-language tags like `[laugh]`, `[whispers]`, and `[super happy]`, as well as native multi-speaker and multi-turn generation.
77
+
78
+ Visit the [Fish Audio website](https://fish.audio/) for live playground. Read the [blog post](https://fish.audio/blog/fish-audio-open-sources-s2/) and [technical report](https://arxiv.org/abs/2603.08823) for more details.
79
+
80
+ ### Model Variants
81
+
82
+ | Model | Size | Availability | Description |
83
+ |------|------|-------------|-------------|
84
+ | S2-Pro | 4B parameters | [HuggingFace](https://huggingface.co/fishaudio/s2-pro) | Full-featured flagship model with maximum quality and stability |
85
+
86
+ More details of the model can be found in the [technical report](https://arxiv.org/abs/2411.01156).
87
+
88
+ ## Benchmark Results
89
+
90
+ | Benchmark | Fish Audio S2 |
91
+ |------|------|
92
+ | Seed-TTS Eval — WER (Chinese) | **0.54%** (best overall) |
93
+ | Seed-TTS Eval — WER (English) | **0.99%** (best overall) |
94
+ | Audio Turing Test (with instruction) | **0.515** posterior mean |
95
+ | EmergentTTS-Eval — Win Rate | **81.88%** (highest overall) |
96
+ | Fish Instruction Benchmark — TAR | **93.3%** |
97
+ | Fish Instruction Benchmark — Quality | **4.51 / 5.0** |
98
+ | Multilingual (MiniMax Testset) — Best WER | **11 of 24** languages |
99
+ | Multilingual (MiniMax Testset) — Best SIM | **17 of 24** languages |
100
+
101
+ On Seed-TTS Eval, S2 achieves the lowest WER among all evaluated models including closed-source systems: Qwen3-TTS (0.77/1.24), MiniMax Speech-02 (0.99/1.90), Seed-TTS (1.12/2.25). On the Audio Turing Test, 0.515 surpasses Seed-TTS (0.417) by 24% and MiniMax-Speech (0.387) by 33%. On EmergentTTS-Eval, S2 achieves particularly strong results in paralinguistics (91.61% win rate), questions (84.41%), and syntactic complexity (83.39%).
102
+
103
+ ## Highlights
104
+
105
+ <img src="../assets/totalability.png" width=200%>
106
+
107
+ ### Fine-Grained Inline Control via Natural Language
108
+
109
+ S2 enables localized control over speech generation by embedding natural-language instructions directly at specific word or phrase positions within the text. Rather than relying on a fixed set of predefined tags, S2 accepts free-form textual descriptions — such as `[whisper in small voice]`, `[professional broadcast tone]`, or `[pitch up]` — allowing open-ended expression control at the word level.
110
+
111
+ ### Dual-Autoregressive Architecture
112
+
113
+ S2 builds on a decoder-only transformer combined with an RVQ-based audio codec (10 codebooks, ~21 Hz frame rate). The Dual-AR architecture splits generation into two stages:
114
+
115
+ - **Slow AR** operates along the time axis and predicts the primary semantic codebook.
116
+ - **Fast AR** generates the remaining 9 residual codebooks at each time step, reconstructing fine-grained acoustic detail.
117
+
118
+ This asymmetric design — 4B parameters along the time axis, 400M parameters along the depth axis — keeps inference efficient while preserving audio fidelity.
119
+
120
+ ### Reinforcement Learning Alignment
121
+
122
+ S2 uses Group Relative Policy Optimization (GRPO) for post-training alignment. The same models used to filter and annotate training data are directly reused as reward models during RL — eliminating distribution mismatch between pre-training data and post-training objectives. The reward signal combines semantic accuracy, instruction adherence, acoustic preference scoring, and timbre similarity.
123
+
124
+ ### Production Streaming via SGLang
125
+
126
+ Because the Dual-AR architecture is structurally isomorphic to standard autoregressive LLMs, S2 directly inherits all LLM-native serving optimizations from SGLang — including continuous batching, paged KV cache, CUDA graph replay, and RadixAttention-based prefix caching.
127
+
128
+ On a single NVIDIA H200 GPU:
129
+
130
+ - **Real-Time Factor (RTF):** 0.195
131
+ - **Time-to-first-audio:** ~100 ms
132
+ - **Throughput:** 3,000+ acoustic tokens/s while maintaining RTF below 0.5
133
+
134
+ ### Multilingual Support
135
+
136
+ S2 supports high-quality multilingual text-to-speech without requiring phonemes or language-specific preprocessing. Including:
137
+
138
+ **English, Chinese, Japanese, Korean, Arabics, German, French...**
139
+
140
+ **AND MORE!**
141
+
142
+ The list is constantly expanding, check [Fish Audio](https://fish.audio/) for the latest releases.
143
+
144
+ ### Native Multi-Speaker Generation
145
+
146
+ <img src="../assets/chattemplate.png" width=200%>
147
+
148
+ Fish Audio S2 allows users to upload reference audio with multi-speaker, the model will deal with every speaker's feature via `<|speaker:i|>` token. Then you can control the model's performance with the speaker id token, allowing a single generation to include multiple speakers. You no longer need to upload reference audio separately for each speaker.
149
+
150
+ ### Multi-Turn Generation
151
+
152
+ Thanks to the expansion of the model context, our model can now use previous information to improve the expressiveness of subsequent generated content, thereby increasing the naturalness of the content.
153
+
154
+ ### Rapid Voice Cloning
155
+
156
+ Fish Audio S2 supports accurate voice cloning using a short reference sample (typically 10–30 seconds). The model captures timbre, speaking style, and emotional tendencies, producing realistic and consistent cloned voices without additional fine-tuning.
157
+ Please refer to [SGLang-Omni README](https://github.com/sgl-project/sglang-omni/blob/main/sglang_omni/models/fishaudio_s2_pro/README.md) to use the SGLang server.
158
+ ---
159
+
160
+ ## Credits
161
+
162
+ - [VITS2 (daniilrobnikov)](https://github.com/daniilrobnikov/vits2)
163
+ - [Bert-VITS2](https://github.com/fishaudio/Bert-VITS2)
164
+ - [GPT VITS](https://github.com/innnky/gpt-vits)
165
+ - [MQTTS](https://github.com/b04901014/MQTTS)
166
+ - [GPT Fast](https://github.com/pytorch-labs/gpt-fast)
167
+ - [GPT-SoVITS](https://github.com/RVC-Boss/GPT-SoVITS)
168
+ - [Qwen3](https://github.com/QwenLM/Qwen3)
169
+
170
+ ## Tech Report
171
+ ```bibtex
172
+ @misc{fish-speech-v1.4,
173
+ title={Fish-Speech: Leveraging Large Language Models for Advanced Multilingual Text-to-Speech Synthesis},
174
+ author={Shijia Liao and Yuxuan Wang and Tianyu Li and Yifan Cheng and Ruoyi Zhang and Rongzhi Zhou and Yijin Xing},
175
+ year={2024},
176
+ eprint={2411.01156},
177
+ archivePrefix={arXiv},
178
+ primaryClass={cs.SD},
179
+ url={https://arxiv.org/abs/2411.01156},
180
+ }
181
+
182
+ @misc{liao2026fishaudios2technical,
183
+ title={Fish Audio S2 Technical Report},
184
+ author={Shijia Liao and Yuxuan Wang and Songting Liu and Yifan Cheng and Ruoyi Zhang and Tianyu Li and Shidong Li and Yisheng Zheng and Xingwei Liu and Qingzheng Wang and Zhizhuo Zhou and Jiahua Liu and Xin Chen and Dawei Han},
185
+ year={2026},
186
+ eprint={2603.08823},
187
+ archivePrefix={arXiv},
188
+ primaryClass={cs.SD},
189
+ url={https://arxiv.org/abs/2603.08823},
190
+ }
191
+ ```
vendor/fish-speech/docs/en/inference.md ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Inference
2
+
3
+ The Fish Audio S2 model requires a large amount of VRAM. We recommend using a GPU with at least 24GB for inference.
4
+
5
+ ## Download Weights
6
+
7
+ First, you need to download the model weights:
8
+
9
+ ```bash
10
+ hf download fishaudio/s2-pro --local-dir checkpoints/s2-pro
11
+ ```
12
+
13
+ ## Command Line Inference
14
+
15
+ !!! note
16
+ If you plan to let the model randomly choose a voice timbre, you can skip this step.
17
+
18
+ ### 1. Get VQ tokens from reference audio
19
+
20
+ ```bash
21
+ python fish_speech/models/dac/inference.py \
22
+ -i "test.wav" \
23
+ --checkpoint-path "checkpoints/s2-pro/codec.pth"
24
+ ```
25
+
26
+ You should get a `fake.npy` and a `fake.wav`.
27
+
28
+ ### 2. Generate Semantic tokens from text:
29
+
30
+ ```bash
31
+ python fish_speech/models/text2semantic/inference.py \
32
+ --text "The text you want to convert" \
33
+ --prompt-text "Your reference text" \
34
+ --prompt-tokens "fake.npy" \
35
+ # --compile
36
+ ```
37
+
38
+ This command will create a `codes_N` file in the working directory, where N is an integer starting from 0.
39
+
40
+ !!! note
41
+ You may want to use `--compile` to fuse CUDA kernels for faster inference. However, we recommend using our sglang inference acceleration optimization.
42
+ Correspondingly, if you do not plan to use acceleration, you can comment out the `--compile` parameter.
43
+
44
+ !!! info
45
+ For GPUs that do not support bf16, you may need to use the `--half` parameter.
46
+
47
+ ### 3. Generate vocals from semantic tokens:
48
+
49
+ ```bash
50
+ python fish_speech/models/dac/inference.py \
51
+ -i "codes_0.npy" \
52
+ ```
53
+
54
+ After that, you will get a `fake.wav` file.
55
+
56
+ ## WebUI Inference
57
+
58
+ ### 1. Gradio WebUI
59
+
60
+ For compatibility, we still maintain the Gradio WebUI.
61
+
62
+ ```bash
63
+ python tools/run_webui.py # --compile if you need acceleration
64
+ ```
65
+
66
+ ### 2. Awesome WebUI
67
+
68
+ Awesome WebUI is a modernized Web interface built with TypeScript, offering richer features and a better user experience.
69
+
70
+ **Build WebUI:**
71
+
72
+ You need to have Node.js and npm installed on your local machine or server.
73
+
74
+ 1. Enter the `awesome_webui` directory:
75
+ ```bash
76
+ cd awesome_webui
77
+ ```
78
+ 2. Install dependencies:
79
+ ```bash
80
+ npm install
81
+ ```
82
+ 3. Build the WebUI:
83
+ ```bash
84
+ npm run build
85
+ ```
86
+
87
+ **Start Backend Server:**
88
+
89
+ After building the WebUI, return to the project root and start the API server:
90
+
91
+ ```bash
92
+ python tools/api_server.py --listen 0.0.0.0:8888 --compile
93
+ ```
94
+
95
+ **Access:**
96
+
97
+ Once the server is running, you can access it via your browser:
98
+ `http://localhost:8888/ui`
vendor/fish-speech/docs/en/install.md ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Requirements
2
+
3
+ - GPU Memory: 24GB (Inference)
4
+ - System: Linux, WSL
5
+
6
+ ## System Setup
7
+
8
+ Fish Audio S2 supports multiple installation methods. Choose the one that best fits your development environment.
9
+
10
+ **Prerequisites**: Install system dependencies for audio processing:
11
+ ``` bash
12
+ apt install portaudio19-dev libsox-dev ffmpeg
13
+ ```
14
+
15
+ ### Conda
16
+
17
+ ```bash
18
+ conda create -n fish-speech python=3.12
19
+ conda activate fish-speech
20
+
21
+ # GPU installation (choose your CUDA version: cu126, cu128, cu129)
22
+ pip install -e .[cu129]
23
+
24
+ # CPU-only installation
25
+ pip install -e .[cpu]
26
+
27
+ # Default installation (uses PyTorch default index)
28
+ pip install -e .
29
+
30
+ # If you encounter an error during installation due to pyaudio, consider using the following command:
31
+ # conda install pyaudio
32
+ # Then run pip install -e . again
33
+ ```
34
+
35
+ ### UV
36
+
37
+ UV provides faster dependency resolution and installation:
38
+
39
+ ```bash
40
+ # GPU installation (choose your CUDA version: cu126, cu128, cu129)
41
+ uv sync --python 3.12 --extra cu129
42
+
43
+ # CPU-only installation
44
+ uv sync --python 3.12 --extra cpu
45
+ ```
46
+ ### Intel Arc XPU support
47
+
48
+ For Intel Arc GPU users, install with XPU support:
49
+
50
+ ```bash
51
+ conda create -n fish-speech python=3.12
52
+ conda activate fish-speech
53
+
54
+ # Install required C++ standard library
55
+ conda install libstdcxx -c conda-forge
56
+
57
+ # Install PyTorch with Intel XPU support
58
+ pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/xpu
59
+
60
+ # Install Fish Speech
61
+ pip install -e .
62
+ ```
63
+
64
+ !!! warning
65
+ The `compile` option is not supported on Windows and macOS. If you want to run with compile, you need to install Triton manually.
66
+
67
+
68
+ ## Docker Setup
69
+
70
+ Fish Audio S2 series model provides multiple Docker deployment options to suit different needs. You can use pre-built images from Docker Hub, build locally with Docker Compose, or manually build custom images.
71
+
72
+ We provide Docker images for both WebUI and API server on both GPU (CUDA126 by default) and CPU. You can use the pre-built images from Docker Hub, build locally with Docker Compose, or manually build custom images. If you want to build locally, follow the instructions below. If you only want to use pre-built images, follow the [inference guide](inference.md).
73
+
74
+ ### Prerequisites
75
+
76
+ - Docker and Docker Compose installed
77
+ - NVIDIA Docker runtime (for GPU support)
78
+ - At least 24GB GPU memory for CUDA inference
79
+
80
+ # Use docker compose
81
+
82
+ For development or customization, you can use Docker Compose to build and run locally:
83
+
84
+ ```bash
85
+ # Clone the repository first
86
+ git clone https://github.com/fishaudio/fish-speech.git
87
+ cd fish-speech
88
+
89
+ # Start WebUI with CUDA
90
+ docker compose --profile webui up
91
+
92
+ # Start WebUI with compile optimization
93
+ COMPILE=1 docker compose --profile webui up
94
+
95
+ # Start API server
96
+ docker compose --profile server up
97
+
98
+ # Start API server with compile optimization
99
+ COMPILE=1 docker compose --profile server up
100
+
101
+ # For CPU-only deployment
102
+ BACKEND=cpu docker compose --profile webui up
103
+ ```
104
+
105
+ #### Environment Variables for Docker Compose
106
+
107
+ You can customize the deployment using environment variables:
108
+
109
+ ```bash
110
+ # .env file example
111
+ BACKEND=cuda # or cpu
112
+ COMPILE=1 # Enable compile optimization
113
+ GRADIO_PORT=7860 # WebUI port
114
+ API_PORT=8080 # API server port
115
+ UV_VERSION=0.8.15 # UV package manager version
116
+ CUDA_VER=12.9.0 # CUDA base image version (e.g. 12.6.0 for older drivers)
117
+ UV_EXTRA=cu129 # PyTorch CUDA variant (cu126, cu128, cu129) — must match CUDA_VER
118
+ ```
119
+
120
+ The command will build the image and run the container. You can access the WebUI at `http://localhost:7860` and the API server at `http://localhost:8080`.
121
+
122
+ ### Manual Docker Build
123
+
124
+ For advanced users who want to customize the build process:
125
+
126
+ ```bash
127
+ # Build WebUI image with CUDA support
128
+ docker build \
129
+ --platform linux/amd64 \
130
+ -f docker/Dockerfile \
131
+ --build-arg BACKEND=cuda \
132
+ --build-arg CUDA_VER=12.9.0 \
133
+ --build-arg UV_EXTRA=cu129 \
134
+ --target webui \
135
+ -t fish-speech-webui:cuda .
136
+
137
+ # Build API server image with CUDA support
138
+ docker build \
139
+ --platform linux/amd64 \
140
+ -f docker/Dockerfile \
141
+ --build-arg BACKEND=cuda \
142
+ --build-arg CUDA_VER=12.9.0 \
143
+ --build-arg UV_EXTRA=cu129 \
144
+ --target server \
145
+ -t fish-speech-server:cuda .
146
+
147
+ # Build CPU-only images (supports multi-platform)
148
+ docker build \
149
+ --platform linux/amd64,linux/arm64 \
150
+ -f docker/Dockerfile \
151
+ --build-arg BACKEND=cpu \
152
+ --target webui \
153
+ -t fish-speech-webui:cpu .
154
+
155
+ # Build development image
156
+ docker build \
157
+ --platform linux/amd64 \
158
+ -f docker/Dockerfile \
159
+ --build-arg BACKEND=cuda \
160
+ --target dev \
161
+ -t fish-speech-dev:cuda .
162
+ ```
163
+
164
+ #### Build Arguments
165
+
166
+ - `BACKEND`: `cuda` or `cpu` (default: `cuda`)
167
+ - `CUDA_VER`: CUDA version (default: `12.6.0`)
168
+ - `UV_EXTRA`: UV extra for CUDA (default: `cu126`)
169
+ - `UBUNTU_VER`: Ubuntu version (default: `24.04`)
170
+ - `PY_VER`: Python version (default: `3.12`)
171
+
172
+ ### Volume Mounts
173
+
174
+ Both methods require mounting these directories:
175
+
176
+ - `./checkpoints:/app/checkpoints` - Model weights directory
177
+ - `./references:/app/references` - Reference audio files directory
178
+
179
+ ### Environment Variables
180
+
181
+ - `COMPILE=1` - Enable torch.compile for faster inference (~10x speedup)
182
+ - `GRADIO_SERVER_NAME=0.0.0.0` - WebUI server host
183
+ - `GRADIO_SERVER_PORT=7860` - WebUI server port
184
+ - `API_SERVER_NAME=0.0.0.0` - API server host
185
+ - `API_SERVER_PORT=8080` - API server port
186
+
187
+ !!! note
188
+ The Docker containers expect model weights to be mounted at `/app/checkpoints`. Make sure to download the required model weights before starting the containers.
189
+
190
+ !!! warning
191
+ GPU support requires NVIDIA Docker runtime. For CPU-only deployment, remove the `--gpus all` flag and use CPU images.
192
+
193
+ ### AMD ROCm support
194
+
195
+ Fish Speech runs on AMD GPUs via ROCm. The ROCm image is based on the official `rocm/pytorch` image, which already ships a gfx-tuned PyTorch, so no separate torch install is needed. Verified on RDNA4 (Radeon AI PRO R9700 / gfx1201) with ROCm 7.2.3; RDNA3 (gfx1100/gfx1101) should also work.
196
+
197
+ **Prerequisites:**
198
+
199
+ - AMD GPU with ROCm support (RDNA3 / RDNA4)
200
+ - ROCm drivers installed on the host
201
+ - Docker with GPU passthrough (`/dev/kfd` and `/dev/dri`)
202
+
203
+ **Using Docker Compose:**
204
+
205
+ ```bash
206
+ # WebUI
207
+ docker compose -f compose.rocm.yml --profile webui up --build
208
+
209
+ # API server
210
+ docker compose -f compose.rocm.yml --profile server up --build
211
+ ```
212
+
213
+ **Manual build and run:**
214
+
215
+ ```bash
216
+ docker build -f docker/Dockerfile.rocm --target webui -t fish-speech-webui:rocm .
217
+
218
+ docker run \
219
+ --device=/dev/kfd --device=/dev/dri \
220
+ --group-add video --group-add render \
221
+ -e ROCBLAS_USE_HIPBLASLT=0 \
222
+ -v ./checkpoints:/app/checkpoints \
223
+ -p 7860:7860 \
224
+ fish-speech-webui:rocm
225
+ ```
226
+
227
+ !!! note
228
+ `ROCBLAS_USE_HIPBLASLT=0` is set by default for RDNA4 (gfx1201) stability; RDNA3 users may not need it. Fish Speech uses `scaled_dot_product_attention`, which dispatches to ROCm's AOTriton flash-attention backend automatically — no custom kernel build is required. The first run is slower while MIOpen auto-tunes kernels. `torch.compile` is enabled by default (`COMPILE=1`); set `COMPILE=0` to disable.
vendor/fish-speech/docs/en/server.md ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Server
2
+
3
+ This page covers server-side inference for Fish Audio S2, plus quick links for WebUI inference and Docker deployment.
4
+
5
+ ## API Server Inference
6
+
7
+ Fish Speech provides an HTTP API server entrypoint at `tools/api_server.py`.
8
+
9
+ ### Start the server locally
10
+
11
+ ```bash
12
+ python tools/api_server.py \
13
+ --llama-checkpoint-path checkpoints/s2-pro \
14
+ --decoder-checkpoint-path checkpoints/s2-pro/codec.pth \
15
+ --listen 0.0.0.0:8080
16
+ ```
17
+
18
+ Common options:
19
+
20
+ - `--compile`: enable `torch.compile` optimization
21
+ - `--half`: use fp16 mode
22
+ - `--api-key`: require bearer token authentication
23
+ - `--workers`: set worker process count
24
+
25
+ ### Health check
26
+
27
+ ```bash
28
+ curl -X GET http://127.0.0.1:8080/v1/health
29
+ ```
30
+
31
+ Expected response:
32
+
33
+ ```json
34
+ {"status":"ok"}
35
+ ```
36
+
37
+ ### Main API endpoint
38
+
39
+ - `POST /v1/tts` for text-to-speech generation
40
+ - `POST /v1/vqgan/encode` for VQ encode
41
+ - `POST /v1/vqgan/decode` for VQ decode
42
+
43
+ ### Python client example
44
+
45
+ The base TTS model is selected when the server starts. In the example above, the server is started with the `checkpoints/s2-pro` weights, so every request sent to `http://127.0.0.1:8080/v1/tts` will use **S2-Pro** automatically. There is no separate per-request `model` field in `tools/api_client.py` for local server calls.
46
+
47
+ ```bash
48
+ python tools/api_client.py \
49
+ --url http://127.0.0.1:8080/v1/tts \
50
+ --text "Hello from Fish Speech" \
51
+ --output s2-pro-demo
52
+ ```
53
+
54
+ If you want to select a saved reference voice, use `--reference_id`. This chooses the **voice reference**, not the base TTS model:
55
+
56
+ ```bash
57
+ python tools/api_client.py \
58
+ --url http://127.0.0.1:8080/v1/tts \
59
+ --text "Hello from Fish Speech" \
60
+ --reference_id my-speaker \
61
+ --output s2-pro-demo
62
+ ```
63
+
64
+ ## WebUI Inference
65
+
66
+ For WebUI usage, see:
67
+
68
+ - [WebUI Inference](https://speech.fish.audio/inference/#webui-inference)
69
+
70
+ ## Docker
71
+
72
+ For Docker-based server or WebUI deployment, see:
73
+
74
+ - [Docker Setup](https://speech.fish.audio/install/#docker-setup)
75
+
76
+ You can also start the server profile directly with Docker Compose:
77
+
78
+ ```bash
79
+ docker compose --profile server up
80
+ ```
vendor/fish-speech/docs/es/finetune.md ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Fine-tuning
2
+
3
+ !!! warning
4
+ Recomendamos encarecidamente no realizar fine-tuning sobre un modelo entrenado con RL. Ajustar un modelo después de RL puede cambiar la distribución del modelo, lo que puede llevar a una degradación del rendimiento.
5
+
6
+ En la versión actual, solo necesitas hacer fine-tuning de la parte ‘LLAMA’.
7
+
8
+ ## Fine-tuning LLAMA
9
+
10
+ ### 1. Preparar el dataset
11
+
12
+ ```
13
+ .
14
+ ├── SPK1
15
+ │ ├── 21.15-26.44.lab
16
+ │ ├── 21.15-26.44.mp3
17
+ │ ├── 27.51-29.98.lab
18
+ │ ├── 27.51-29.98.mp3
19
+ │ ├── 30.1-32.71.lab
20
+ │ └── 30.1-32.71.mp3
21
+ └── SPK2
22
+ ├── 38.79-40.85.lab
23
+ └── 38.79-40.85.mp3
24
+ ```
25
+
26
+ Necesitas convertir tu dataset al formato anterior y colocarlo dentro de `data`. El archivo de audio puede tener extensiones `.mp3`, `.wav` o `.flac`, y el archivo de anotación debe tener la extensión `.lab`.
27
+
28
+ !!! info
29
+ El archivo de anotación `.lab` solo necesita contener la transcripción del audio, sin ningún formato especial. Por ejemplo, si `hi.mp3` dice "Hello, goodbye," entonces el archivo `hi.lab` contendría una única línea de texto: "Hello, goodbye."
30
+
31
+ !!! warning
32
+ Se recomienda aplicar normalización de loudness al dataset. Puedes usar [fish-audio-preprocess](https://github.com/fishaudio/audio-preprocess) para hacerlo.
33
+
34
+ ````
35
+ ```bash
36
+ fap loudness-norm data-raw data --clean
37
+ ```
38
+ ````
39
+
40
+ ### 2. Extracción por lotes de tokens semánticos
41
+
42
+ Asegúrate de haber descargado los pesos de VQGAN. Si no, ejecuta el siguiente comando:
43
+
44
+ ```bash
45
+ huggingface-cli download fishaudio/openaudio-s1-mini --local-dir checkpoints/openaudio-s1-mini
46
+ ```
47
+
48
+ Luego puedes ejecutar el siguiente comando para extraer los tokens semánticos:
49
+
50
+ ```bash
51
+ python tools/vqgan/extract_vq.py data \
52
+ --num-workers 1 --batch-size 16 \
53
+ --config-name "modded_dac_vq" \
54
+ --checkpoint-path "checkpoints/openaudio-s1-mini/codec.pth"
55
+ ```
56
+
57
+ !!! note
58
+ Puedes ajustar `--num-workers` y `--batch-size` para aumentar la velocidad de extracción, pero asegúrate de no exceder el límite de memoria de tu GPU.
59
+
60
+ Este comando creará archivos `.npy` en el directorio `data`, como se muestra a continuación:
61
+
62
+ ```
63
+ .
64
+ ├── SPK1
65
+ │ ├── 21.15-26.44.lab
66
+ │ ├── 21.15-26.44.mp3
67
+ │ ├── 21.15-26.44.npy
68
+ │ ├── 27.51-29.98.lab
69
+ │ ├── 27.51-29.98.mp3
70
+ │ ├── 27.51-29.98.npy
71
+ │ ├── 30.1-32.71.lab
72
+ │ ├── 30.1-32.71.mp3
73
+ │ └── 30.1-32.71.npy
74
+ └── SPK2
75
+ ├── 38.79-40.85.lab
76
+ ├── 38.79-40.85.mp3
77
+ └── 38.79-40.85.npy
78
+ ```
79
+
80
+ ### 3. Empaquetar el dataset en protobuf
81
+
82
+ ```bash
83
+ python tools/llama/build_dataset.py \
84
+ --input "data" \
85
+ --output "data/protos" \
86
+ --text-extension .lab \
87
+ --num-workers 16
88
+ ```
89
+
90
+ Después de que el comando termine de ejecutarse, deberías ver el archivo `protos` en el directorio `data`.
91
+
92
+ ### 4. Finalmente, fine-tuning con LoRA
93
+
94
+ De manera similar, asegúrate de haber descargado los pesos de `LLAMA`. Si no, ejecuta el siguiente comando:
95
+
96
+ ```bash
97
+ huggingface-cli download fishaudio/openaudio-s1-mini --local-dir checkpoints/openaudio-s1-mini
98
+ ```
99
+
100
+ Finalmente, puedes comenzar el fine-tuning ejecutando el siguiente comando:
101
+
102
+ ```bash
103
+ python fish_speech/train.py --config-name text2semantic_finetune \
104
+ project=$project \
105
+ +lora@model.model.lora_config=r_8_alpha_16
106
+ ```
107
+
108
+ !!! note
109
+ Puedes modificar parámetros de entrenamiento como `batch_size`, `gradient_accumulation_steps`, etc., para ajustarlos a la memoria de tu GPU editando `fish_speech/configs/text2semantic_finetune.yaml`.
110
+
111
+ !!! note
112
+ Para usuarios de Windows, puedes usar `trainer.strategy.process_group_backend=gloo` para evitar problemas con `nccl`.
113
+
114
+ Una vez que el entrenamiento esté completo, puedes consultar la sección de [inference](inference.md) para probar tu modelo.
115
+
116
+ !!! info
117
+ Por defecto, el modelo solo aprenderá los patrones de habla del hablante y no el timbre. Aún necesitas usar prompts para asegurar la estabilidad del timbre.
118
+ Si quieres aprender el timbre, puedes aumentar el número de pasos de entrenamiento, pero esto puede llevar a overfitting.
119
+
120
+ Después del entrenamiento, necesitas convertir los pesos LoRA a pesos normales antes de realizar inferencia.
121
+
122
+ ```bash
123
+ python tools/llama/merge_lora.py \
124
+ --lora-config r_8_alpha_16 \
125
+ --base-weight checkpoints/openaudio-s1-mini \
126
+ --lora-weight results/$project/checkpoints/step_000000010.ckpt \
127
+ --output checkpoints/openaudio-s1-mini-yth-lora/
128
+ ```
129
+
130
+ !!! note
131
+ También puedes probar otros checkpoints. Sugerimos usar el checkpoint más temprano que cumpla con tus requisitos, ya que suelen rendir mejor en datos fuera de distribución (OOD).
vendor/fish-speech/docs/es/index.md ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+ <h1>Fish Speech</h1>
3
+
4
+ <p><a href="../en/">English</a> | <a href="../zh/">简体中文</a> | <a href="../pt/">Portuguese</a> | <a href="../ja/">日本語</a> | <a href="../ko/">한국어</a> | <a href="../ar/">العربية</a> | <strong>Español</strong></p>
5
+
6
+ <a href="https://www.producthunt.com/products/fish-speech?embed=true&utm_source=badge-top-post-badge&utm_medium=badge&utm_source=badge-fish&#0045;audio&#0045;s1" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=1023740&theme=light&period=daily&t=1761164814710" alt="Fish&#0032;Audio&#0032;S1 - Clonación&#0032;de&#0032;voz&#0032;expresiva&#0032;y&#0032;texto&#0045;a&#0045;voz | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://trendshift.io/repositories/7014" target="_blank"> <img src="https://trendshift.io/api/badge/repositories/7014" alt="fishaudio%2Ffish-speech | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/> </a>
7
+
8
+ </div>
9
+
10
+ <br>
11
+
12
+ <div align="center">
13
+ <img src="https://count.getloli.com/get/@fish-speech?theme=asoul" /><br>
14
+ </div>
15
+
16
+ <br>
17
+
18
+ <div align="center">
19
+ <a target="_blank" href="https://discord.gg/Es5qTB9BcN">
20
+ <img alt="Discord" src="https://img.shields.io/discord/1214047546020728892?color=%23738ADB&label=Discord&logo=discord&logoColor=white&style=flat-square"/>
21
+ </a>
22
+ <a target="_blank" href="https://hub.docker.com/r/fishaudio/fish-speech">
23
+ <img alt="Docker" src="https://img.shields.io/docker/pulls/fishaudio/fish-speech?style=flat-square&logo=docker"/>
24
+ </a>
25
+ <a target="_blank" href="https://pd.qq.com/s/bwxia254o">
26
+ <img alt="QQ Channel" src="https://img.shields.io/badge/QQ-blue?logo=tencentqq">
27
+ </a>
28
+ </div>
29
+
30
+ <div align="center">
31
+ <a target="_blank" href="https://huggingface.co/fishaudio/s2">
32
+ <img alt="HuggingFace Model" src="https://img.shields.io/badge/🤗%20-models-orange"/>
33
+ </a>
34
+ <a target="_blank" href="https://fish.audio/blog/fish-audio-open-sources-s2/">
35
+ <img alt="Fish Audio Blog" src="https://img.shields.io/badge/Blog-Fish_Audio_S2-1f7a8c?style=flat-square&logo=readme&logoColor=white"/>
36
+ </a>
37
+ <a target="_blank" href="https://github.com/fishaudio/fish-speech/blob/main/FishAudioS2TecReport.pdf">
38
+ <img alt="Paper | Technical Report" src="https://img.shields.io/badge/Paper-Technical_Report-b31b1b?style=flat-square"/>
39
+ </a>
40
+ </div>
41
+
42
+ !!! info "Aviso de Licencia"
43
+ Este código y sus pesos de modelo asociados se publican bajo **FISH AUDIO RESEARCH LICENSE**. Por favor, consulta [LICENSE](https://github.com/fishaudio/fish-speech/blob/main/LICENSE) para más detalles. Tomaremos acciones contra cualquier violación de la licencia.
44
+
45
+ !!! warning "Aviso Legal"
46
+ No asumimos ninguna responsabilidad por el uso ilegal de este código. Por favor, consulta las leyes locales sobre DMCA y otras leyes relacionadas.
47
+
48
+ ## Inicio Rápido
49
+
50
+ ### Para Humanos
51
+
52
+ Aquí están los documentos oficiales de Fish Audio S2, sigue las instrucciones para comenzar fácilmente.
53
+
54
+ * [Instalación](https://speech.fish.audio/install/)
55
+ * [Inferencia por Línea de Comandos](https://speech.fish.audio/inference/#command-line-inference)
56
+ * [Inferencia con WebUI](https://speech.fish.audio/inference/#webui-inference)
57
+ * [Inferencia en Servidor](https://speech.fish.audio/server/)
58
+ * [Configuración con Docker](https://speech.fish.audio/install/#docker-setup)
59
+
60
+ > [!IMPORTANT]
61
+ > **Para el servidor SGLang, por favor lee [SGLang-Omni README](https://github.com/sgl-project/sglang-omni/blob/main/sglang_omni/models/fishaudio_s2_pro/README.md).**
62
+
63
+ ### Para Agente LLM
64
+
65
+ ```
66
+ Instala y configura Fish-Audio S2 siguiendo las instrucciones aquí: https://speech.fish.audio/install/
67
+ ```
68
+
69
+ ## Fish Audio S2
70
+
71
+ **Mejor sistema de texto a voz entre código abierto y cerrado**
72
+
73
+ Fish Audio S2 es el modelo más reciente desarrollado por [Fish Audio](https://fish.audio/). Entrenado con más de 10 millones de horas de audio en aproximadamente 50 idiomas, S2 combina alineación mediante aprendizaje por refuerzo con una arquitectura Dual-Autoregresiva para generar voz natural, realista y emocionalmente rica.
74
+
75
+ S2 soporta control detallado en línea de prosodia y emoción usando etiquetas en lenguaje natural como `[laugh]`, `[whispers]` y `[super happy]`, así como generación nativa multi-hablante y multi-turno.
76
+
77
+ Visita el [sitio web de Fish Audio](https://fish.audio/) para el playground en vivo. Lee el [blog](https://fish.audio/blog/fish-audio-open-sources-s2/) para más detalles.
78
+
79
+ ### Variantes del Modelo
80
+
81
+ | Modelo | Tamaño | Disponibilidad | Descripción |
82
+ | ------ | ------------- | ------------------------------------------------------ | --------------------------------------------------------------------------- |
83
+ | S2-Pro | 4B parámetros | [HuggingFace](https://huggingface.co/fishaudio/s2-pro) | Modelo insignia con todas las funcionalidades, máxima calidad y estabilidad |
84
+
85
+ Más detalles del modelo pueden encontrarse en el [reporte técnico](https://arxiv.org/abs/2411.01156).
86
+
87
+ ## Resultados de Benchmark
88
+
89
+ | Benchmark | Fish Audio S2 |
90
+ | ------------------------------------------- | ---------------------------- |
91
+ | Seed-TTS Eval — WER (Chino) | **0.54%** (mejor global) |
92
+ | Seed-TTS Eval — WER (Inglés) | **0.99%** (mejor global) |
93
+ | Test de Turing de Audio (con instrucción) | **0.515** media posterior |
94
+ | EmergentTTS-Eval — Win Rate | **81.88%** (más alto global) |
95
+ | Fish Instruction Benchmark — TAR | **93.3%** |
96
+ | Fish Instruction Benchmark — Calidad | **4.51 / 5.0** |
97
+ | Multilenguaje (MiniMax Testset) — Mejor WER | **11 de 24** idiomas |
98
+ | Multilenguaje (MiniMax Testset) — Mejor SIM | **17 de 24** idiomas |
99
+
100
+ En Seed-TTS Eval, S2 logra el menor WER entre todos los modelos evaluados incluyendo sistemas cerrados: Qwen3-TTS (0.77/1.24), MiniMax Speech-02 (0.99/1.90), Seed-TTS (1.12/2.25). En el Test de Turing de Audio, 0.515 supera a Seed-TTS (0.417) en un 24% y a MiniMax-Speech (0.387) en un 33%. En EmergentTTS-Eval, S2 obtiene resultados particularmente fuertes en paralingüística (91.61% win rate), preguntas (84.41%) y complejidad sintáctica (83.39%).
101
+
102
+ ## Características Destacadas
103
+
104
+ <img src="../assets/totalability.png" width=200%>
105
+
106
+ ### Control Fino en Línea mediante Lenguaje Natural
107
+
108
+ S2 permite control localizado sobre la generación de voz incrustando instrucciones en lenguaje natural directamente en posiciones específicas del texto. En lugar de depender de un conjunto fijo de etiquetas, S2 acepta descripciones libres como `[whisper in small voice]`, `[professional broadcast tone]` o `[pitch up]`, permitiendo control expresivo abierto a nivel de palabra.
109
+
110
+ ### Arquitectura Dual-Autoregresiva
111
+
112
+ S2 se basa en un transformer solo-decoder combinado con un códec de audio basado en RVQ (10 codebooks, ~21 Hz). La arquitectura Dual-AR divide la generación en dos etapas:
113
+
114
+ * **AR Lento** opera en el eje temporal y predice el codebook semántico principal.
115
+ * **AR Rápido** genera los 9 codebooks residuales restantes en cada paso temporal, reconstruyendo detalles acústicos finos.
116
+
117
+ Este diseño asimétrico — 4B parámetros en el eje temporal y 400M en el eje de profundidad — mantiene la inferencia eficiente sin perder fidelidad.
118
+
119
+ ### Alineación con Aprendizaje por Refuerzo
120
+
121
+ S2 utiliza Group Relative Policy Optimization (GRPO) para alineación post-entrenamiento. Los mismos modelos usados para filtrar y anotar datos se reutilizan como modelos de recompensa durante RL, eliminando desajustes de distribución. La señal de recompensa combina precisión semántica, cumplimiento de instrucciones, preferencia acústica y similitud de timbre.
122
+
123
+ ### Streaming en Producción con SGLang
124
+
125
+ Debido a que la arquitectura Dual-AR es estructuralmente isomórfica a los LLM autoregresivos estándar, S2 hereda optimizaciones como batching continuo, caché KV paginado, CUDA graph replay y caching de prefijos basado en RadixAttention.
126
+
127
+ En una sola GPU NVIDIA H200:
128
+
129
+ * **RTF:** 0.195
130
+ * **Tiempo hasta primer audio:** ~100 ms
131
+ * **Throughput:** 3,000+ tokens acústicos/s manteniendo RTF < 0.5
132
+
133
+ ### Soporte Multilenguaje
134
+
135
+ S2 soporta texto a voz multilenguaje de alta calidad sin requerir fonemas ni preprocesamiento específico del idioma. Incluye:
136
+
137
+ **Inglés, Chino, Japonés, Coreano, Árabe, Alemán, Francés...**
138
+
139
+ **¡Y MÁS!**
140
+
141
+ La lista continúa expandiéndose, revisa [Fish Audio](https://fish.audio/) para las últimas novedades.
142
+
143
+ ### Generación Multi-Hablante Nativa
144
+
145
+ <img src="../assets/chattemplate.png" width=200%>
146
+
147
+ Fish Audio S2 permite a los usuarios subir audio de referencia con múltiples hablantes; el modelo gestionará las características de cada uno mediante el token `<|speaker:i|>`. Luego puedes controlar el comportamiento del modelo con ese identificador, permitiendo múltiples voces en una sola generación. Ya no necesitas subir audio por separado para cada hablante.
148
+
149
+ ### Generación Multi-Turno
150
+
151
+ Gracias a la expansión del contexto del modelo, ahora puede usar información previa para mejorar la expresividad del contenido generado, aumentando la naturalidad.
152
+
153
+ ### Clonación de Voz Rápida
154
+
155
+ Fish Audio S2 permite clonación de voz precisa usando una muestra corta (10–30 segundos). El modelo captura timbre, estilo y emoción, produciendo voces realistas y consistentes sin fine-tuning adicional.
156
+ Por favor consulta [SGLang-Omni README](https://github.com/sgl-project/sglang-omni/blob/main/sglang_omni/models/fishaudio_s2_pro/README.md) para usar el servidor SGLang.
157
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------
158
+
159
+ ## Créditos
160
+
161
+ * [VITS2 (daniilrobnikov)](https://github.com/daniilrobnikov/vits2)
162
+ * [Bert-VITS2](https://github.com/fishaudio/Bert-VITS2)
163
+ * [GPT VITS](https://github.com/innnky/gpt-vits)
164
+ * [MQTTS](https://github.com/b04901014/MQTTS)
165
+ * [GPT Fast](https://github.com/pytorch-labs/gpt-fast)
166
+ * [GPT-SoVITS](https://github.com/RVC-Boss/GPT-SoVITS)
167
+ * [Qwen3](https://github.com/QwenLM/Qwen3)
168
+
169
+ ## Reporte Técnico
170
+
171
+ ```bibtex
172
+ @misc{fish-speech-v1.4,
173
+ title={Fish-Speech: Leveraging Large Language Models for Advanced Multilingual Text-to-Speech Synthesis},
174
+ author={Shijia Liao and Yuxuan Wang and Tianyu Li and Yifan Cheng and Ruoyi Zhang and Rongzhi Zhou and Yijin Xing},
175
+ year={2024},
176
+ eprint={2411.01156},
177
+ archivePrefix={arXiv},
178
+ primaryClass={cs.SD},
179
+ url={https://arxiv.org/abs/2411.01156},
180
+ }
181
+
182
+ @misc{liao2026fishaudios2technical,
183
+ title={Fish Audio S2 Technical Report},
184
+ author={Shijia Liao and Yuxuan Wang and Songting Liu and Yifan Cheng and Ruoyi Zhang and Tianyu Li and Shidong Li and Yisheng Zheng and Xingwei Liu and Qingzheng Wang and Zhizhuo Zhou and Jiahua Liu and Xin Chen and Dawei Han},
185
+ year={2026},
186
+ eprint={2603.08823},
187
+ archivePrefix={arXiv},
188
+ primaryClass={cs.SD},
189
+ url={https://arxiv.org/abs/2603.08823},
190
+ }
191
+ ```
vendor/fish-speech/docs/es/inference.md ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Inferencia
2
+
3
+ El modelo Fish Audio S2 requiere una gran cantidad de VRAM. Recomendamos usar una GPU con al menos 24GB para la inferencia.
4
+
5
+ ## Descargar Pesos
6
+
7
+ Primero, necesitas descargar los pesos del modelo:
8
+
9
+ ```bash
10
+ hf download fishaudio/s2-pro --local-dir checkpoints/s2-pro
11
+ ```
12
+
13
+ ## Inferencia por Línea de Comandos
14
+
15
+ !!! note
16
+ Si planeas dejar que el modelo elija aleatoriamente el timbre de voz, puedes omitir este paso.
17
+
18
+ ### 1. Obtener tokens VQ a partir de audio de referencia
19
+
20
+ ```bash
21
+ python fish_speech/models/dac/inference.py \
22
+ -i "test.wav" \
23
+ --checkpoint-path "checkpoints/s2-pro/codec.pth"
24
+ ```
25
+
26
+ Deberías obtener un `fake.npy` y un `fake.wav`.
27
+
28
+ ### 2. Generar tokens semánticos a partir de texto:
29
+
30
+ ```bash
31
+ python fish_speech/models/text2semantic/inference.py \
32
+ --text "El texto que quieres convertir" \
33
+ --prompt-text "Tu texto de referencia" \
34
+ --prompt-tokens "fake.npy" \
35
+ # --compile
36
+ ```
37
+
38
+ Este comando creará un archivo `codes_N` en el directorio de trabajo, donde N es un entero que comienza desde 0.
39
+
40
+ !!! note
41
+ Puede que quieras usar `--compile` para fusionar kernels CUDA y acelerar la inferencia. Sin embargo, recomendamos usar nuestra optimización de aceleración de inferencia con sglang.
42
+ Correspondientemente, si no planeas usar aceleración, puedes comentar el parámetro `--compile`.
43
+
44
+ !!! info
45
+ Para GPUs que no soportan bf16, puede que necesites usar el parámetro `--half`.
46
+
47
+ ### 3. Generar audio a partir de tokens semánticos:
48
+
49
+ ```bash
50
+ python fish_speech/models/dac/inference.py \
51
+ -i "codes_0.npy" \
52
+ ```
53
+
54
+ Después de eso, obtendrás un archivo `fake.wav`.
55
+
56
+ ## Inferencia con WebUI
57
+
58
+ Próximamente.