Daporte commited on
Commit
380b209
·
verified ·
1 Parent(s): c3e6eff

Upload conformer.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. conformer.py +113 -0
conformer.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run the f0+intensity -> mel conformer.
2
+
3
+ The real module, loaded from the trained checkpoint. This is the path the
4
+ published numbers come from: `generate()` sizes its output from the real
5
+ input length and takes a padding mask, so a phrase is predicted at its own
6
+ duration.
7
+
8
+ The alternative was the ONNX export, whose input width is baked in at 2000
9
+ frames -- every phrase padded to ten seconds, with the decoder attending
10
+ over the padding. Measured against the training pipeline's own predictions
11
+ that is 0.9357 correlation against 0.9985 here, which is two orders of
12
+ magnitude more than the f0 extraction differs by, so it is not a reasonable
13
+ substitute when the point is reproducing the paper.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from pathlib import Path
20
+
21
+ import numpy as np
22
+
23
+ # Keys FeaturesToMelConv's constructor accepts. model_config.json carries
24
+ # training settings alongside them, and the model calls it mel_dim rather
25
+ # than mel_bins and takes no max_len -- so guessing the set does not work.
26
+ CONSTRUCTOR_KEYS = {
27
+ "input_dim", "d_model", "nhead", "num_encoder_layers",
28
+ "num_decoder_layers", "encoder_kernel_size", "conv_kernel_size",
29
+ "dim_feedforward", "dropout", "loss_type", "length_ratio",
30
+ "gradient_checkpointing", "decoder_only", "attn_window",
31
+ "mel_dim",
32
+ }
33
+
34
+
35
+ class Conformer:
36
+ """Predicts a mel spectrogram from 200 Hz prosody features."""
37
+
38
+ def __init__(self, checkpoint: Path | str, cpu: bool = False) -> None:
39
+ """`checkpoint` is a directory holding model_config.json and weights.
40
+
41
+ The weights may sit beside the config or in a `checkpoint-*`
42
+ subdirectory, which is how the training runs leave them.
43
+ """
44
+ import torch
45
+ from safetensors.torch import load_file
46
+
47
+ from .conformer_model import FeaturesToMelConv
48
+
49
+ root = Path(checkpoint)
50
+ config_path = root / "model_config.json"
51
+ if not config_path.exists():
52
+ raise FileNotFoundError(
53
+ f"{config_path} not found. The conformer needs the directory "
54
+ f"holding model_config.json and its weights."
55
+ )
56
+
57
+ config = json.loads(config_path.read_text())
58
+ kwargs = {k: v for k, v in config.items() if k in CONSTRUCTOR_KEYS}
59
+
60
+ weights = sorted(root.glob("model.safetensors"))
61
+ weights += sorted(root.glob("checkpoint-*/model.safetensors"))
62
+ if not weights:
63
+ raise FileNotFoundError(f"no model.safetensors under {root}")
64
+
65
+ self._model = FeaturesToMelConv(**kwargs)
66
+ # strict=False because the checkpoint carries training-only buffers
67
+ # the inference classes do not declare. Checked against the ONNX
68
+ # export's output, which is what catches a genuinely wrong load.
69
+ self._model.load_state_dict(load_file(str(weights[-1])), strict=False)
70
+ self._model.eval()
71
+
72
+ self._device = torch.device(
73
+ "cpu" if cpu or not torch.cuda.is_available() else "cuda"
74
+ )
75
+ self._model.to(self._device)
76
+
77
+ self.mel_bins: int = int(config.get("mel_dim", config.get("mel_bins", 12)))
78
+ self.length_ratio: float = float(config.get("length_ratio", 0.5))
79
+ self._source = str(weights[-1])
80
+
81
+ @property
82
+ def provider(self) -> str:
83
+ return f"torch ({self._device.type})"
84
+
85
+ @property
86
+ def max_duration_s(self) -> float:
87
+ """No fixed window: generate() sizes its output from the input."""
88
+ return float("inf")
89
+
90
+ def predict(self, features: np.ndarray) -> np.ndarray:
91
+ """[T, 2] normalised features -> [T*length_ratio, mel_bins] mel.
92
+
93
+ The padding mask is all-false: a single phrase has nothing padded,
94
+ and passing the mask is what lets generate() size the output from
95
+ the real length rather than from a fixed window.
96
+ """
97
+ import torch
98
+
99
+ features = np.asarray(features, dtype=np.float32)
100
+ if features.ndim != 2 or features.shape[1] != 2:
101
+ raise ValueError(f"expected [T, 2] features, got {features.shape}")
102
+ if features.size == 0:
103
+ return np.zeros((0, self.mel_bins), dtype=np.float32)
104
+
105
+ with torch.no_grad():
106
+ batch = torch.from_numpy(features).unsqueeze(0).to(self._device)
107
+ mask = torch.zeros(
108
+ (1, batch.shape[1]), dtype=torch.bool, device=self._device
109
+ )
110
+ out = self._model.generate(batch, input_padding_mask=mask)
111
+
112
+ mel = (out[0] if isinstance(out, tuple) else out)[0]
113
+ return mel.detach().cpu().numpy().astype(np.float32)