Swagcrew commited on
Commit
e18d852
·
verified ·
1 Parent(s): ba6dc8e

Upload scripts/job_quantize.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. scripts/job_quantize.py +450 -0
scripts/job_quantize.py ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Fish Speech S2 Pro — Quantization Experiment (HF Job version)
4
+ Downloads model, applies quantization at all phases, generates voice clone samples.
5
+ """
6
+ import os, sys, json, time, gc, traceback
7
+ import torch
8
+ import torch.nn as nn
9
+ import numpy as np
10
+
11
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
12
+ DEVICE = "cuda"
13
+ DTYPE = torch.bfloat16
14
+ BASE_MODEL = "fishaudio/s2-pro"
15
+ OUT = "/app/output"
16
+
17
+ print("=== Fish Speech S2 Pro Quantization Experiment ===")
18
+ print(f"PyTorch: {torch.__version__}, CUDA: {torch.cuda.is_available()}")
19
+ print(f"GPU: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'none'}")
20
+ print(f"VRAM: {torch.cuda.get_device_properties(0).total_mem / 1e9:.1f} GB" if torch.cuda.is_available() else "")
21
+
22
+ # Install deps
23
+ print("\n[1/8] Installing dependencies...")
24
+ os.system("pip install -q einops loguru ormsgpack hydra-core omegaconf safetensors torchaudio soundfile")
25
+ os.system("pip install -q datasets")
26
+
27
+ # Clone fish-speech
28
+ if not os.path.exists("/app/fish-speech"):
29
+ print("\n[2/8] Cloning fish-speech repo...")
30
+ os.system("cd /app && git clone --depth 1 https://github.com/fishaudio/fish-speech.git")
31
+ else:
32
+ print("\n[2/8] fish-speech already cloned")
33
+ sys.path.insert(0, "/app/fish-speech")
34
+
35
+ # Download model
36
+ print("\n[3/8] Downloading S2 Pro model...")
37
+ os.system(f"huggingface-cli download {BASE_MODEL} --local-dir /app/checkpoints/s2-pro")
38
+
39
+ BASE_MODEL = "/app/checkpoints/s2-pro"
40
+
41
+ # ============ QUANTIZATION CLASSES ============
42
+
43
+ class FP8Linear(nn.Module):
44
+ def __init__(self, in_f, out_f, bias=True):
45
+ super().__init__()
46
+ self.in_features, self.out_features = in_f, out_f
47
+ self.register_buffer("weight", torch.empty(out_f, in_f, dtype=torch.float8_e4m3fn))
48
+ self.register_buffer("weight_scale", torch.empty(out_f, 1, dtype=torch.float32))
49
+ self.has_bias = bias
50
+ if bias: self.register_buffer("bias", torch.zeros(out_f, dtype=torch.bfloat16))
51
+ else: self.bias = None
52
+
53
+ @staticmethod
54
+ def from_linear(linear):
55
+ fp8 = FP8Linear(linear.in_features, linear.out_features, linear.bias is not None)
56
+ w = linear.weight.data.detach().bfloat16()
57
+ scale = w.abs().amax(dim=1, keepdim=True).clamp(min=1e-12) / 448.0
58
+ w_q = (w / scale).round().clamp(-448, 448).to(torch.float8_e4m3fn)
59
+ fp8.weight.data.copy_(w_q)
60
+ fp8.weight_scale.data.copy_(scale)
61
+ if linear.bias is not None:
62
+ fp8.bias.data.copy_(linear.bias.data.detach().bfloat16())
63
+ return fp8
64
+
65
+ def forward(self, x):
66
+ return nn.functional.linear(x, self.weight.to(torch.bfloat16) * self.weight_scale, self.bias)
67
+
68
+ class INT8Linear(nn.Module):
69
+ def __init__(self, in_f, out_f, bias=True):
70
+ super().__init__()
71
+ self.in_features, self.out_features = in_f, out_f
72
+ self.register_buffer("weight", torch.empty(out_f, in_f, dtype=torch.int8))
73
+ self.register_buffer("weight_scale", torch.empty(out_f, 1, dtype=torch.float32))
74
+ self.has_bias = bias
75
+ if bias: self.register_buffer("bias", torch.zeros(out_f, dtype=torch.bfloat16))
76
+ else: self.bias = None
77
+
78
+ @staticmethod
79
+ def from_linear(linear):
80
+ q = INT8Linear(linear.in_features, linear.out_features, linear.bias is not None)
81
+ w = linear.weight.data.detach().bfloat16()
82
+ scale = w.abs().amax(dim=1, keepdim=True).clamp(min=1e-12) / 127.0
83
+ q.weight.data.copy_((w / scale).round().clamp(-128, 127).to(torch.int8))
84
+ q.weight_scale.data.copy_(scale)
85
+ if linear.bias is not None: q.bias.data.copy_(linear.bias.data.detach().bfloat16())
86
+ return q
87
+
88
+ def forward(self, x):
89
+ return nn.functional.linear(x, self.weight.to(torch.bfloat16) * self.weight_scale, self.bias)
90
+
91
+ class INT4Linear(nn.Module):
92
+ def __init__(self, in_f, out_f, group_size=128, bias=True):
93
+ super().__init__()
94
+ self.in_features, self.out_features, self.group_size = in_f, out_f, group_size
95
+ self.register_buffer("weight_q", torch.empty(out_f, in_f, dtype=torch.int8))
96
+ self.register_buffer("weight_scale", torch.empty(out_f, (in_f + group_size - 1) // group_size, dtype=torch.float32))
97
+ self.has_bias = bias
98
+ if bias: self.register_buffer("bias", torch.zeros(out_f, dtype=torch.bfloat16))
99
+ else: self.bias = None
100
+
101
+ @staticmethod
102
+ def from_linear(linear, group_size=128):
103
+ in_f, out_f = linear.in_features, linear.out_features
104
+ q = INT4Linear(in_f, out_f, group_size, linear.bias is not None)
105
+ w = linear.weight.data.detach().bfloat16()
106
+ n_groups = (in_f + group_size - 1) // group_size
107
+ pad = n_groups * group_size - in_f
108
+ if pad > 0: w = nn.functional.pad(w, (0, pad))
109
+ w_g = w.reshape(out_f, n_groups, group_size)
110
+ scale = w_g.abs().amax(dim=-1, keepdim=True).clamp(min=1e-10) / 7.0
111
+ q.weight_q.data.copy_((w_g / scale).round().clamp(-7, 7).to(torch.int8).reshape(out_f, -1)[:, :in_f])
112
+ q.weight_scale.data.copy_(scale.squeeze(-1)[:, :n_groups])
113
+ if linear.bias is not None: q.bias.data.copy_(linear.bias.data.detach().bfloat16())
114
+ return q
115
+
116
+ def forward(self, x):
117
+ s = self.weight_scale.repeat_interleave(self.group_size, dim=1)[:, :self.in_features]
118
+ return nn.functional.linear(x, self.weight_q[:, :self.in_features].to(torch.bfloat16) * s, self.bias)
119
+
120
+ class INT3Linear(nn.Module):
121
+ def __init__(self, in_f, out_f, group_size=128, bias=True):
122
+ super().__init__()
123
+ self.in_features, self.out_features, self.group_size = in_f, out_f, group_size
124
+ self.register_buffer("weight_q", torch.empty(out_f, in_f, dtype=torch.int8))
125
+ self.register_buffer("weight_scale", torch.empty(out_f, (in_f + group_size - 1) // group_size, dtype=torch.float32))
126
+ self.has_bias = bias
127
+ if bias: self.register_buffer("bias", torch.zeros(out_f, dtype=torch.bfloat16))
128
+ else: self.bias = None
129
+
130
+ @staticmethod
131
+ def from_linear(linear, group_size=128):
132
+ in_f, out_f = linear.in_features, linear.out_features
133
+ q = INT3Linear(in_f, out_f, group_size, linear.bias is not None)
134
+ w = linear.weight.data.detach().bfloat16()
135
+ n_groups = (in_f + group_size - 1) // group_size
136
+ pad = n_groups * group_size - in_f
137
+ if pad > 0: w = nn.functional.pad(w, (0, pad))
138
+ w_g = w.reshape(out_f, n_groups, group_size)
139
+ scale = w_g.abs().amax(dim=-1, keepdim=True).clamp(min=1e-10) / 3.0
140
+ q.weight_q.data.copy_((w_g / scale).round().clamp(-3, 3).to(torch.int8).reshape(out_f, -1)[:, :in_f])
141
+ q.weight_scale.data.copy_(scale.squeeze(-1)[:, :n_groups])
142
+ if linear.bias is not None: q.bias.data.copy_(linear.bias.data.detach().bfloat16())
143
+ return q
144
+
145
+ def forward(self, x):
146
+ s = self.weight_scale.repeat_interleave(self.group_size, dim=1)[:, :self.in_features]
147
+ return nn.functional.linear(x, self.weight_q[:, :self.in_features].to(torch.bfloat16) * s, self.bias)
148
+
149
+ class INT2Linear(nn.Module):
150
+ def __init__(self, in_f, out_f, group_size=64, bias=True):
151
+ super().__init__()
152
+ self.in_features, self.out_features, self.group_size = in_f, out_f, group_size
153
+ self.register_buffer("weight_q", torch.empty(out_f, in_f, dtype=torch.int8))
154
+ self.register_buffer("weight_scale", torch.empty(out_f, (in_f + group_size - 1) // group_size, dtype=torch.float32))
155
+ self.has_bias = bias
156
+ if bias: self.register_buffer("bias", torch.zeros(out_f, dtype=torch.bfloat16))
157
+ else: self.bias = None
158
+
159
+ @staticmethod
160
+ def from_linear(linear, group_size=64):
161
+ in_f, out_f = linear.in_features, linear.out_features
162
+ q = INT2Linear(in_f, out_f, group_size, linear.bias is not None)
163
+ w = linear.weight.data.detach().bfloat16()
164
+ n_groups = (in_f + group_size - 1) // group_size
165
+ pad = n_groups * group_size - in_f
166
+ if pad > 0: w = nn.functional.pad(w, (0, pad))
167
+ w_g = w.reshape(out_f, n_groups, group_size)
168
+ scale = w_g.abs().amax(dim=-1, keepdim=True).clamp(min=1e-10) / 1.0
169
+ q.weight_q.data.copy_((w_g / scale).round().clamp(-1, 1).to(torch.int8).reshape(out_f, -1)[:, :in_f])
170
+ q.weight_scale.data.copy_(scale.squeeze(-1)[:, :n_groups])
171
+ if linear.bias is not None: q.bias.data.copy_(linear.bias.data.detach().bfloat16())
172
+ return q
173
+
174
+ def forward(self, x):
175
+ s = self.weight_scale.repeat_interleave(self.group_size, dim=1)[:, :self.in_features]
176
+ return nn.functional.linear(x, self.weight_q[:, :self.in_features].to(torch.bfloat16) * s, self.bias)
177
+
178
+
179
+ # ============ HELPERS ============
180
+
181
+ def apply_quant(model, qcls, target="slow_ar", **kw):
182
+ count = 0
183
+ skip = ['embed', 'norm']
184
+ for name, mod in list(model.named_modules()):
185
+ if not isinstance(mod, nn.Linear): continue
186
+ if any(s in name for s in skip): continue
187
+ if target == "slow_ar" and "fast_" in name: continue
188
+ parts = name.split(".")
189
+ parent = model
190
+ for p in parts[:-1]: parent = getattr(parent, p)
191
+ try:
192
+ setattr(parent, parts[-1], qcls.from_linear(mod, **kw))
193
+ count += 1
194
+ except: pass
195
+ return model, count
196
+
197
+ def model_size_mb(m):
198
+ t = sum(p.numel() * p.element_size() for p in m.parameters())
199
+ t += sum(b.numel() * b.element_size() for b in m.buffers())
200
+ return t / (1024*1024)
201
+
202
+ def generate_sample(model, codec, text, out_path):
203
+ """Generate TTS sample"""
204
+ import soundfile as sf
205
+ from fish_speech.models.text2semantic.inference import generate, decode_one_token_ar
206
+ from fish_speech.content_sequence import TextPart
207
+ from fish_speech.conversation import Conversation, Message
208
+
209
+ try:
210
+ conv = Conversation()
211
+ conv.add_message(Message(role="user", parts=[TextPart(text="")]))
212
+ conv.add_message(Message(role="assistant", parts=[TextPart(text=text)]))
213
+ prompt = conv.encode_for_inference(model.config)
214
+ cd = 1 + model.config.num_codebooks
215
+ am = torch.zeros(1, cd, prompt.shape[-1], dtype=torch.bool, device=DEVICE)
216
+ ap = torch.zeros(1, cd, prompt.shape[-1], dtype=torch.long, device=DEVICE)
217
+
218
+ if not getattr(model, '_cache_done', False):
219
+ model.setup_caches(1, model.config.max_seq_len, dtype=DTYPE)
220
+ model._cache_done = True
221
+
222
+ with torch.autocast(device_type="cuda", dtype=DTYPE):
223
+ result = generate(model=model, prompt=prompt, max_new_tokens=512,
224
+ audio_masks=am, audio_parts=ap, temperature=0.7, top_p=0.7, top_k=30,
225
+ decode_one_token=decode_one_token_ar)
226
+
227
+ codes = result[0:1, :, :].unsqueeze(0)
228
+ with torch.autocast(device_type="cuda", dtype=DTYPE):
229
+ audio = codec.decode(codes.to(DEVICE))
230
+ audio_np = audio.squeeze().cpu().float().numpy()
231
+ sr = getattr(codec, 'sample_rate', 44100)
232
+ sf.write(out_path, audio_np, sr)
233
+ dur = len(audio_np) / sr
234
+ print(f" Saved {out_path} ({dur:.1f}s)")
235
+ return True, dur
236
+ except Exception as e:
237
+ print(f" Sample gen failed: {e}")
238
+ traceback.print_exc()
239
+ return False, 0
240
+
241
+ def generate_clone(model, codec, text, ref_path, ref_text, out_path):
242
+ """Voice clone sample"""
243
+ import torchaudio, soundfile as sf
244
+ from fish_speech.models.text2semantic.inference import generate, decode_one_token_ar
245
+ from fish_speech.content_sequence import TextPart, VQPart
246
+ from fish_speech.conversation import Conversation, Message
247
+
248
+ try:
249
+ wav, sr = torchaudio.load(ref_path)
250
+ if wav.shape[0] > 1: wav = wav.mean(dim=0, keepdim=True)
251
+ if sr != 44100: wav = torchaudio.functional.resample(wav, sr, 44100)
252
+ wav = wav.to(DEVICE)
253
+ with torch.autocast(device_type="cuda", dtype=DTYPE):
254
+ enc = codec.encode(wav.unsqueeze(0))
255
+ ptokens = (enc[0] if isinstance(enc, tuple) else enc).cpu().numpy()
256
+
257
+ conv = Conversation()
258
+ conv.add_message(Message(role="user", parts=[VQPart(codes=ptokens), TextPart(text=ref_text)]))
259
+ conv.add_message(Message(role="assistant", parts=[TextPart(text=text)]))
260
+ prompt = conv.encode_for_inference(model.config)
261
+ cd = 1 + model.config.num_codebooks
262
+ am = torch.zeros(1, cd, prompt.shape[-1], dtype=torch.bool, device=DEVICE)
263
+ ap = torch.zeros(1, cd, prompt.shape[-1], dtype=torch.long, device=DEVICE)
264
+
265
+ if not getattr(model, '_cache_done', False):
266
+ model.setup_caches(1, model.config.max_seq_len, dtype=DTYPE)
267
+ model._cache_done = True
268
+
269
+ with torch.autocast(device_type="cuda", dtype=DTYPE):
270
+ result = generate(model=model, prompt=prompt, max_new_tokens=512,
271
+ audio_masks=am, audio_parts=ap, temperature=0.7, top_p=0.7, top_k=30,
272
+ decode_one_token=decode_one_token_ar)
273
+ codes = result[0:1, :, :].unsqueeze(0)
274
+ with torch.autocast(device_type="cuda", dtype=DTYPE):
275
+ audio = codec.decode(codes.to(DEVICE))
276
+ audio_np = audio.squeeze().cpu().float().numpy()
277
+ sr = getattr(codec, 'sample_rate', 44100)
278
+ sf.write(out_path, audio_np, sr)
279
+ dur = len(audio_np) / sr
280
+ print(f" Clone saved {out_path} ({dur:.1f}s)")
281
+ return True, dur
282
+ except Exception as e:
283
+ print(f" Clone failed: {e}")
284
+ traceback.print_exc()
285
+ return False, 0
286
+
287
+
288
+ # ============ PHASE RUNNER ============
289
+
290
+ def run_phase(pid, qcls, target, codec, ref_path, ref_text, test_text, clone_text, **kw):
291
+ from fish_speech.models.text2semantic.inference import init_model
292
+ from safetensors.torch import save_file
293
+
294
+ phase_dir = f"{OUT}/{pid}"
295
+ os.makedirs(phase_dir, exist_ok=True)
296
+ os.makedirs(f"{OUT}/samples", exist_ok=True)
297
+
298
+ print(f"\n{'='*60}")
299
+ print(f" {pid.upper()}: {qcls.__name__} ({target})")
300
+ print(f"{'='*60}")
301
+
302
+ model, _ = init_model(BASE_MODEL, DEVICE, DTYPE, compile=False)
303
+ orig = model_size_mb(model)
304
+
305
+ t0 = time.time()
306
+ model, nl = apply_quant(model, qcls, target=target, **kw)
307
+ model = model.to(DEVICE)
308
+ t_q = time.time() - t0
309
+ qs = model_size_mb(model)
310
+ ratio = orig / qs if qs > 0 else 0
311
+ print(f" {orig:.0f} -> {qs:.0f} MB ({ratio:.2f}x, {nl} layers, {t_q:.1f}s)")
312
+
313
+ sp = f"{phase_dir}/model.safetensors"
314
+ save_file(model.state_dict(), sp)
315
+ disk = os.path.getsize(sp) / (1024*1024)
316
+ print(f" Disk: {disk:.0f} MB")
317
+
318
+ tts_ok, tts_d = generate_sample(model, codec, test_text, f"{OUT}/samples/{pid}_tts.wav")
319
+ clone_ok, clone_d = False, 0
320
+ if ref_path and os.path.exists(ref_path):
321
+ clone_ok, clone_d = generate_clone(model, codec, clone_text, ref_path, ref_text, f"{OUT}/samples/{pid}_clone.wav")
322
+
323
+ del model; gc.collect(); torch.cuda.empty_cache()
324
+
325
+ r = {"phase": pid, "method": qcls.__name__, "target": target,
326
+ "orig_mb": round(orig), "quant_mb": round(qs), "disk_mb": round(disk),
327
+ "ratio": round(ratio, 3), "layers": nl, "time_s": round(t_q,1),
328
+ "tts_ok": tts_ok, "tts_d": round(tts_d,1),
329
+ "clone_ok": clone_ok, "clone_d": round(clone_d,1)}
330
+ with open(f"{phase_dir}/results.json","w") as f: json.dump(r,f,indent=2)
331
+ return r
332
+
333
+ # ============ MAIN ============
334
+
335
+ TEST_TEXT = "The quick brown fox jumps over the lazy dog. Artificial intelligence is transforming how we communicate."
336
+ CLONE_TEXT = "Hello everyone, welcome to this special presentation. Today we explore the fascinating world of neural text to speech and voice cloning technology."
337
+ REF_TEXT = "This is a reference voice recording."
338
+ CELEB_TEXT = "Good morning. I want to tell you something about the universe. Every atom in your body came from a star that exploded. We are all made of star stuff."
339
+
340
+ PHASES = {
341
+ "1a": (FP8Linear, "slow_ar", {}),
342
+ "1b": (INT4Linear, "slow_ar", {"group_size": 128}),
343
+ "2a": (INT4Linear, "all", {"group_size": 128}),
344
+ "2b": (INT8Linear, "slow_ar", {}),
345
+ "2c": (INT3Linear, "slow_ar", {"group_size": 128}),
346
+ "3a": (INT2Linear, "slow_ar", {"group_size": 64}),
347
+ "3b": (INT2Linear, "all", {"group_size": 64}),
348
+ }
349
+
350
+ def main():
351
+ os.makedirs(f"{OUT}/samples", exist_ok=True)
352
+ from fish_speech.models.text2semantic.inference import init_model
353
+ from fish_speech.models.dac.inference import load_codec_model
354
+
355
+ print("\n[4/8] Loading base model...")
356
+ model_base, _ = init_model(BASE_MODEL, DEVICE, DTYPE, compile=False)
357
+ orig = model_size_mb(model_base)
358
+ print(f" Base model: {orig:.0f} MB ({sum(p.numel() for p in model_base.parameters())/1e9:.2f}B params)")
359
+
360
+ print("\n[5/8] Loading codec...")
361
+ codec = load_codec_model(f"{BASE_MODEL}/codec.pth", DEVICE, DTYPE)
362
+
363
+ # Generate reference + baseline
364
+ ref_path = f"{OUT}/reference_celebrity.wav"
365
+ print("\n[6/8] Generating reference audio & baseline...")
366
+ try:
367
+ generate_sample(model_base, codec, CELEB_TEXT, ref_path)
368
+ except Exception as e:
369
+ print(f" Ref gen warning: {e}")
370
+ ref_path = None
371
+
372
+ try:
373
+ generate_sample(model_base, codec, TEST_TEXT, f"{OUT}/samples/baseline_bf16_tts.wav")
374
+ except: pass
375
+
376
+ if ref_path:
377
+ try:
378
+ generate_clone(model_base, codec, CLONE_TEXT, ref_path, REF_TEXT, f"{OUT}/samples/baseline_bf16_clone.wav")
379
+ except: pass
380
+
381
+ del model_base; gc.collect(); torch.cuda.empty_cache()
382
+
383
+ # Run all phases
384
+ print("\n[7/8] Running quantization phases...")
385
+ all_r = [{"phase": "baseline_bf16", "orig_mb": round(orig), "quant_mb": round(orig), "disk_mb": round(orig), "ratio": 1.0}]
386
+
387
+ for pid, (qcls, target, kw) in PHASES.items():
388
+ try:
389
+ r = run_phase(f"phase{pid}", qcls, target, codec, ref_path, REF_TEXT, TEST_TEXT, CLONE_TEXT, **kw)
390
+ all_r.append(r)
391
+ except Exception as e:
392
+ print(f"Phase {pid} FAILED: {e}")
393
+ traceback.print_exc()
394
+ all_r.append({"phase": f"phase{pid}", "error": str(e)})
395
+
396
+ # Summary
397
+ print(f"\n{'='*70}")
398
+ print(" RESULTS SUMMARY")
399
+ print(f"{'='*70}")
400
+ print(f"{'Phase':<14} {'Method':<14} {'Disk MB':<10} {'Ratio':<8} {'TTS':<5} {'Clone':<5}")
401
+ print("-"*60)
402
+ for r in all_r:
403
+ print(f"{r.get('phase','?'):<14} {r.get('method','bf16'):<14} {r.get('disk_mb','?'):<10} {r.get('ratio',1):<8.2f} {'OK' if r.get('tts_ok') else 'FAIL':<5} {'OK' if r.get('clone_ok') else 'FAIL':<5}")
404
+
405
+ with open(f"{OUT}/all_results.json","w") as f: json.dump(all_r,f,indent=2)
406
+
407
+ # Upload to Hub
408
+ print("\n[8/8] Uploading results to HuggingFace Hub...")
409
+ try:
410
+ from huggingface_hub import HfApi
411
+ api = HfApi()
412
+ repo = "Swagcrew/fish-speech-s2-quantized"
413
+ api.create_repo(repo_id=repo, repo_type="model", exist_ok=True, private=False)
414
+
415
+ # Upload all results
416
+ api.upload_file(path_or_fileobj=f"{OUT}/all_results.json", path_in_repo="all_results.json", repo_id=repo, repo_type="model")
417
+
418
+ # Upload samples
419
+ samples_dir = f"{OUT}/samples"
420
+ if os.path.exists(samples_dir):
421
+ for fn in os.listdir(samples_dir):
422
+ if fn.endswith(".wav"):
423
+ api.upload_file(path_or_fileobj=os.path.join(samples_dir, fn),
424
+ path_in_repo=f"samples/{fn}", repo_id=repo, repo_type="model")
425
+
426
+ # Upload individual phase results
427
+ for pid in PHASES:
428
+ phase_dir = f"{OUT}/phase{pid}"
429
+ if os.path.exists(f"{phase_dir}/results.json"):
430
+ api.upload_file(f"{phase_dir}/results.json", f"phase{pid}/results.json", repo, repo_type="model")
431
+ if os.path.exists(f"{phase_dir}/model.safetensors"):
432
+ api.upload_file(f"{phase_dir}/model.safetensors", f"phase{pid}/model.safetensors", repo, repo_type="model")
433
+
434
+ # Upload README
435
+ readme = """# Fish Speech S2 Pro — Quantization Experiments
436
+ Multi-phase quantization with voice cloning samples.
437
+ See all_results.json for details and samples/ for audio.
438
+ """
439
+ api.upload_file(path_or_fileobj=readme.encode(), path_in_repo="README.md", repo_id=repo, repo_type="model")
440
+
441
+ print(f" Uploaded to https://huggingface.co/{repo}")
442
+ except Exception as e:
443
+ print(f" Upload failed: {e}")
444
+ traceback.print_exc()
445
+
446
+ print("\nDONE! All phases complete.")
447
+
448
+ if __name__ == "__main__":
449
+ main()
450
+