Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- runtime-data/references/.gitkeep +1 -0
- runtime/experimental/__init__.py +1 -0
- runtime/experimental/codec.py +336 -0
- runtime/native/LICENSE +202 -0
- runtime/native/direct_w4a4_m1/direct_w4a4_m1.cpp +104 -0
- runtime/native/rmsnorm_nvfp4_m1/rmsnorm_nvfp4_m1.cpp +38 -0
- runtime/native/rmsnorm_nvfp4_m1/rmsnorm_nvfp4_m1.cu +160 -0
- runtime/native/rmsnorm_nvfp4_m1/rmsnorm_nvfp4_m1.h +10 -0
- runtime/native/smallm_gemv/smallm_gemv.cpp +86 -0
- runtime/native/smallm_gemv/smallm_gemv.cu +171 -0
- runtime/native/smallm_gemv/smallm_gemv.h +12 -0
- runtime/web/index.html +234 -0
- vendor/fish-speech/.github/ISSUE_TEMPLATE/bug_report.yml +63 -0
- vendor/fish-speech/.github/ISSUE_TEMPLATE/config.yml +5 -0
- vendor/fish-speech/.github/ISSUE_TEMPLATE/feature_request.yml +64 -0
- vendor/fish-speech/.github/workflows/build-docker-image.yml +78 -0
- vendor/fish-speech/.github/workflows/docs.yml +33 -0
- vendor/fish-speech/.github/workflows/stale.yml +25 -0
- vendor/fish-speech/awesome_webui/public/vite.svg +1 -0
- vendor/fish-speech/awesome_webui/src/App.tsx +1185 -0
- vendor/fish-speech/awesome_webui/src/assets/react.svg +1 -0
- vendor/fish-speech/awesome_webui/src/components/ui/alert.tsx +35 -0
- vendor/fish-speech/awesome_webui/src/components/ui/badge.tsx +31 -0
- vendor/fish-speech/awesome_webui/src/components/ui/button.tsx +44 -0
- vendor/fish-speech/awesome_webui/src/components/ui/card.tsx +31 -0
- vendor/fish-speech/awesome_webui/src/components/ui/collapsible.tsx +7 -0
- vendor/fish-speech/awesome_webui/src/components/ui/dialog.tsx +86 -0
- vendor/fish-speech/awesome_webui/src/components/ui/label.tsx +15 -0
- vendor/fish-speech/awesome_webui/src/components/ui/scroll-area.tsx +43 -0
- vendor/fish-speech/awesome_webui/src/components/ui/separator.tsx +26 -0
- vendor/fish-speech/awesome_webui/src/components/ui/slider.tsx +23 -0
- vendor/fish-speech/awesome_webui/src/components/ui/switch.tsx +27 -0
- vendor/fish-speech/awesome_webui/src/components/ui/textarea.tsx +17 -0
- vendor/fish-speech/awesome_webui/src/components/ui/toggle-group.tsx +49 -0
- vendor/fish-speech/awesome_webui/src/index.css +71 -0
- vendor/fish-speech/awesome_webui/src/lib/utils.ts +6 -0
- vendor/fish-speech/awesome_webui/src/main.tsx +10 -0
- vendor/fish-speech/docs/ar/finetune.md +125 -0
- vendor/fish-speech/docs/ar/index.md +192 -0
- vendor/fish-speech/docs/ar/inference.md +98 -0
- vendor/fish-speech/docs/ar/install.md +189 -0
- vendor/fish-speech/docs/assets/logo.svg +29 -0
- vendor/fish-speech/docs/en/finetune.md +128 -0
- vendor/fish-speech/docs/en/index.md +191 -0
- vendor/fish-speech/docs/en/inference.md +98 -0
- vendor/fish-speech/docs/en/install.md +228 -0
- vendor/fish-speech/docs/en/server.md +80 -0
- vendor/fish-speech/docs/es/finetune.md +131 -0
- vendor/fish-speech/docs/es/index.md +191 -0
- 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-audio-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 Audio S1 - Expressive Voice Cloning and Text-to-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-audio-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 Audio S1 - Expressive Voice Cloning and Text-to-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-audio-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 Audio S1 - Clonación de voz expresiva y texto-a-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.
|