File size: 9,524 Bytes
df83106
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
#!/usr/bin/env python3
"""mio_codec_wrapper.py

Wrapper to run MioCodec in two modes:
 - streaming: connects to a websocket (e.g. vLLM/your LLM) that emits
   token embeddings (JSON messages) and decodes them to audio in near
   real-time using the windowed streamer.
 - offline: loads a JSONL file of embeddings and decodes to a WAV file.

Requirements (minimal): torch, numpy, websockets (for streaming), soundfile (for offline write), sounddevice (optional playback).

Install quickly without cloning full repo (if upstream exists):
  pip install git+https://github.com/Aratako/MioCodec.git

If you already have local helper files (`streaming_istft_head.py`, `windowed_streamer.py`), the wrapper will import them; otherwise it will try to use `miocodec` package's model and swap in the streaming head.
"""
from __future__ import annotations

import argparse
import asyncio
import json
import sys
from pathlib import Path

import numpy as np
import torch


def build_args():
    p = argparse.ArgumentParser()
    p.add_argument("--mode", choices=("stream", "offline"), default="stream")
    p.add_argument("--uri", default="ws://localhost:8000/stream", help="Websocket URI for streaming mode")
    p.add_argument("--input", help="JSONL file with embeddings for offline mode")
    p.add_argument("--out", default="out.wav", help="Output WAV file for offline mode")
    p.add_argument("--device", default="cpu")
    p.add_argument("--repo-id", default="Aratako/MioCodec-25Hz-24kHz")
    p.add_argument("--play", action="store_true", help="Play decoded audio while streaming")
    p.add_argument("--chunk-size", type=int, default=20)
    p.add_argument("--left", type=int, default=10)
    p.add_argument("--right", type=int, default=10)
    return p.parse_args()


def load_model_and_streamer(repo_id: str, device: str, chunk_size: int, left: int, right: int):
    """Return (model, streamer, sample_rate). Attempts to import local windowed_streamer first."""
    try:
        # Prefer local helper module if present
        from windowed_streamer import build_windowed_model, MioCodecWindowedStreamer, WindowedStreamConfig

        model = build_windowed_model(repo_id=repo_id, device=device)
        cfg = model.config
        stream_cfg = WindowedStreamConfig(chunk_size=chunk_size, left_context_tokens=left, right_lookahead_tokens=right)
        # create a default global embedding (zero) sized to model's expected dim
        gdim = getattr(cfg, "global_embedding_dim", getattr(cfg, "wave_decoder_dim", 512))
        global_embedding = torch.zeros(gdim, dtype=torch.float32).to(device)
        streamer = MioCodecWindowedStreamer(model, global_embedding, config=stream_cfg)
        return model, streamer, getattr(cfg, "sample_rate", 24000)
    except Exception:
        # Fallback: import MioCodecModel from installed package and swap head
        from miocodec.model import MioCodecModel
        try:
            from streaming_istft_head import ISTFTHead as StreamingISTFTHead
        except Exception:
            StreamingISTFTHead = None

        model = MioCodecModel.from_pretrained(repo_id).to(device)
        cfg = model.config
        if StreamingISTFTHead is not None:
            old_head = model.istft_head
            new_head = StreamingISTFTHead(dim=cfg.wave_decoder_dim, n_fft=cfg.n_fft, hop_length=cfg.hop_length, padding=cfg.istft_padding).to(device)
            new_head.out.load_state_dict(old_head.out.state_dict())
            model.istft_head = new_head

        # create a simple streamer-like helper using WindowedStreamConfig from windowed_streamer if available
        try:
            from windowed_streamer import MioCodecWindowedStreamer, WindowedStreamConfig

            stream_cfg = WindowedStreamConfig(chunk_size=chunk_size, left_context_tokens=left, right_lookahead_tokens=right)
            gdim = getattr(cfg, "global_embedding_dim", getattr(cfg, "wave_decoder_dim", 512))
            global_embedding = torch.zeros(gdim, dtype=torch.float32).to(device)
            streamer = MioCodecWindowedStreamer(model, global_embedding, config=stream_cfg)
        except Exception:
            # Minimal fallback: use a naive one-shot decoder wrapper for full sequences
            streamer = None

        return model, streamer, getattr(cfg, "sample_rate", 24000)


async def stream_mode(uri: str, model, streamer, samplerate: int, play: bool, device: str):
    """Connect to websocket; expect messages with {"embedding": [...]} or {"finished": true}."""
    try:
        import websockets
    except Exception:
        print("Install websockets: pip install websockets", file=sys.stderr)
        return

    sd = None
    if play:
        try:
            import sounddevice as sd
        except Exception:
            sd = None

    async with websockets.connect(uri) as ws:
        print("connected", uri)
        async for raw in ws:
            try:
                msg = json.loads(raw)
            except Exception:
                continue

            if msg.get("finished"):
                if streamer is not None:
                    final = streamer.finish()
                    if final is not None:
                        arr = final.cpu().numpy().astype(np.float32)
                        if sd is not None:
                            sd.play(arr, samplerate)
                            sd.wait()
                        else:
                            print("final chunk ready (not played)")
                break

            emb = msg.get("embedding")
            if emb is None:
                continue
            tensor = torch.tensor(emb, dtype=torch.float32).to(device)
            if streamer is not None:
                audio = streamer.push_token(tensor)
                if audio is not None:
                    arr = audio.cpu().numpy().astype(np.float32)
                    if sd is not None:
                        sd.play(arr, samplerate)
                        # do not wait to allow continuous streaming
                    else:
                        sys.stdout.buffer.write(arr.tobytes())
            else:
                # no streamer: buffer and decode once finished
                # simple approach: accumulate embeddings on model directly if model supports method
                if not hasattr(stream_mode, "buffer"):
                    stream_mode.buffer = []
                stream_mode.buffer.append(tensor.cpu().numpy())


def write_wav(path: Path, arr: np.ndarray, samplerate: int):
    try:
        import soundfile as sf

        sf.write(str(path), arr, samplerate)
    except Exception:
        # fallback: write raw float32
        path.write_bytes(arr.astype(np.float32).tobytes())


def offline_mode(input_path: Path, out_path: Path, model, streamer, samplerate: int, device: str):
    """Read JSONL file of {embedding: [...]} lines and decode offline."""
    embeddings = []
    with input_path.open("r") as f:
        for line in f:
            try:
                m = json.loads(line)
            except Exception:
                continue
            if "embedding" in m:
                embeddings.append(torch.tensor(m["embedding"], dtype=torch.float32).to(device))

    if streamer is not None:
        # push all and finish
        for emb in embeddings:
            streamer.push_token(emb)
        final = streamer.finish()
        if final is not None:
            arr = final.cpu().numpy().astype(np.float32)
            write_wav(out_path, arr, samplerate)
            print("Wrote", out_path)
            return

    # fallback one-shot: try to concatenate and run model-specific decode path
    if hasattr(model, "istft_head") and hasattr(model, "wave_prenet"):
        # build a content embedding and run forward once (best-effort)
        all_tokens = torch.stack(embeddings, dim=0).unsqueeze(0).to(device)
        with torch.no_grad():
            local_latent = model.wave_prenet(all_tokens)
            if model.wave_conv_upsample is not None:
                local_latent = model.wave_conv_upsample(local_latent.transpose(1, 2)).transpose(1, 2)
            # approximate frames
            frames = local_latent.shape[1] * 100  # heuristic; prefer streamer for correctness
            local_latent = torch.nn.functional.interpolate(local_latent.transpose(1, 2), size=frames, mode=model.config.wave_interpolation_mode).transpose(1, 2)
            local_latent = model.wave_prior_net(local_latent.transpose(1, 2)).transpose(1, 2)
            local_latent = model.wave_decoder(local_latent, condition=torch.zeros(1, 1, local_latent.shape[-1], device=device))
            local_latent = model.wave_post_net(local_latent.transpose(1, 2)).transpose(1, 2)
            audio = model.istft_head(local_latent).squeeze(0).cpu().numpy().astype(np.float32)
            write_wav(out_path, audio, samplerate)
            print("Wrote", out_path)
            return

    print("Could not decode offline: no streamer and model lacks decode path.")


def main():
    args = build_args()
    device = args.device
    model, streamer, samplerate = load_model_and_streamer(args.repo_id, device, args.chunk_size, args.left, args.right)

    if args.mode == "stream":
        asyncio.run(stream_mode(args.uri, model, streamer, samplerate, args.play, device))
    else:
        if not args.input:
            print("--input required for offline mode", file=sys.stderr)
            return
        offline_mode(Path(args.input), Path(args.out), model, streamer, samplerate, device)


if __name__ == "__main__":
    main()