File size: 15,842 Bytes
61cc64e
 
8be9381
 
 
 
 
3c46eac
61cc64e
8be9381
61cc64e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8be9381
 
61cc64e
 
8be9381
61cc64e
 
 
 
 
 
 
 
 
8be9381
 
6ff0f7c
949f371
8be9381
 
61cc64e
 
 
 
 
8be9381
 
 
 
eb96c1a
61cc64e
eb96c1a
8be9381
eb96c1a
8be9381
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61cc64e
 
8be9381
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eb96c1a
8be9381
 
 
 
 
 
 
 
 
 
 
 
6ff0f7c
 
 
 
 
8be9381
6ff0f7c
8be9381
6ff0f7c
8be9381
 
eb96c1a
5bfae80
 
 
 
5cd697f
61cc64e
5cd697f
61cc64e
 
 
 
5cd697f
61cc64e
 
 
 
5cd697f
61cc64e
 
 
 
 
 
 
 
5cd697f
61cc64e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5cd697f
8be9381
5cd697f
 
 
 
 
6ff0f7c
61cc64e
6ff0f7c
8be9381
 
 
2a53af7
61cc64e
2a53af7
 
 
61cc64e
2a53af7
 
 
 
 
 
61cc64e
8be9381
eb96c1a
61cc64e
8be9381
 
eb96c1a
 
 
8be9381
6ff0f7c
 
8be9381
 
 
61cc64e
 
5bfae80
 
 
61cc64e
8be9381
 
 
 
5bfae80
61cc64e
8be9381
5bfae80
8be9381
61cc64e
8be9381
 
 
 
 
 
5bfae80
 
 
 
eb96c1a
5cd697f
 
 
949f371
5cd697f
61cc64e
 
 
 
 
 
 
 
 
3c46eac
61cc64e
 
5cd697f
61cc64e
3c46eac
 
61cc64e
8be9381
 
eb96c1a
 
8be9381
 
 
 
 
eb96c1a
6ff0f7c
eb96c1a
6ff0f7c
8be9381
 
 
6ff0f7c
 
 
 
 
 
 
 
 
8be9381
6ff0f7c
eb96c1a
8be9381
6ff0f7c
eb96c1a
6ff0f7c
eb96c1a
8be9381
 
 
eb96c1a
 
8be9381
eb96c1a
 
6ff0f7c
 
eb96c1a
 
 
8be9381
 
 
6ff0f7c
 
 
 
 
 
 
 
 
 
eb96c1a
6ff0f7c
8be9381
 
 
 
5bfae80
8be9381
 
61cc64e
2a53af7
 
61cc64e
5cd697f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2a53af7
 
5cd697f
2a53af7
5cd697f
0c86095
d529b42
5cd697f
8be9381
 
6ff0f7c
8be9381
 
 
6ff0f7c
 
61cc64e
8be9381
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
"""RVC Voice Conversion – HuggingFace Space

Simple, fast, GPU/CPU auto-detected.
"""
from __future__ import annotations

import os

import gradio as gr

from lib.config import (
    BUILTIN_MODELS,
    CSS,
    DEVICE_LABEL,
    MAX_INPUT_DURATION,
    logger,
)
from lib.jobs import (
    get_jobs_table,
    get_queue_info,
    poll_job,
    submit_job,
)
from lib.models import list_models, startup_downloads
from lib.ui import refresh_models, toggle_autotune, upload_model

# ── Startup ───────────────────────────────────────────────────────────────────
startup_status = ""
default_model = ""
try:
    default_model = startup_downloads()
    startup_status = f"βœ… Ready  Β·  {DEVICE_LABEL}"
except Exception as e:
    startup_status = f"⚠️ Some assets unavailable: {e}  Β·  {DEVICE_LABEL}"
    logger.warning("Startup download issue: %s", e)

initial_models = list_models()
initial_value = default_model if default_model in initial_models else (
    initial_models[0] if initial_models else None
)

# ── Gradio UI ─────────────────────────────────────────────────────────────────
with gr.Blocks(title="RVC Voice Conversion", delete_cache=(3600, 3600)) as demo:

    gr.HTML(f"""
    <div id="header">
        <h1>πŸŽ™οΈ RVC Voice Conversion</h1>
        <p>Retrieval-Based Voice Conversion Β· record or upload Β· custom models Β· GPU/CPU auto</p>
    </div>
    <p id="status">{startup_status}</p>
    """)

    with gr.Tabs():

        # ── TAB 1: Convert ────────────────────────────────────────────────────
        with gr.Tab("🎀 Convert"):
            with gr.Row():

                with gr.Column(scale=1):
                    gr.Markdown("### πŸ”Š Input Audio")
                    with gr.Tabs():
                        with gr.Tab("πŸŽ™οΈ Microphone"):
                            inp_mic = gr.Audio(
                                sources=["microphone"],
                                type="filepath",
                                label="Record",
                            )
                        with gr.Tab("πŸ“ Upload File"):
                            inp_file = gr.Audio(
                                sources=["upload"],
                                type="filepath",
                                label="Upload audio (wav / mp3 / flac / ogg …)",
                            )

                    gr.Markdown("### πŸ€– Model")
                    model_dd = gr.Dropdown(
                        choices=initial_models,
                        value=initial_value,
                        label="Active Voice Model",
                        interactive=True,
                    )

                    gr.Markdown("### 🎚️ Basic Settings")
                    pitch_sl = gr.Slider(
                        minimum=-24, maximum=24, value=0, step=1,
                        label="Pitch Shift (semitones)",
                        info="0 = unchanged Β· positive = higher Β· negative = lower",
                    )
                    f0_radio = gr.Radio(
                        choices=["rmvpe", "fcpe", "crepe", "crepe-tiny"],
                        value="rmvpe",
                        label="Pitch Extraction Method",
                        info="rmvpe = fastest & accurate Β· crepe = highest quality (slower)",
                    )

                with gr.Column(scale=1):
                    gr.Markdown("### βš™οΈ Advanced Settings")
                    with gr.Accordion("Expand advanced options", open=False):
                        index_rate_sl = gr.Slider(
                            0.0, 1.0, value=0.75, step=0.05,
                            label="Index Rate",
                            info="How strongly the FAISS index influences timbre (0 = off)",
                        )
                        protect_sl = gr.Slider(
                            0.0, 0.5, value=0.5, step=0.01,
                            label="Protect Consonants",
                            info="Protects unvoiced consonants β€” 0.5 = max protection",
                        )
                        filter_radius_sl = gr.Slider(
                            0, 7, value=3, step=1,
                            label="Respiration Filter Radius",
                            info="Median filter on pitch β€” higher = smoother, reduces breath noise",
                        )
                        vol_env_sl = gr.Slider(
                            0.0, 1.0, value=0.25, step=0.05,
                            label="Volume Envelope Mix",
                            info="0.25 = natural blend Β· 1 = preserve input loudness Β· 0 = model output",
                        )
                        with gr.Row():
                            clean_cb = gr.Checkbox(value=False, label="Noise Reduction")
                            clean_sl = gr.Slider(
                                0.0, 1.0, value=0.5, step=0.05,
                                label="Reduction Strength",
                            )
                        with gr.Row():
                            split_cb = gr.Checkbox(value=False, label="Split Long Audio")
                            autotune_cb = gr.Checkbox(value=False, label="Autotune")
                            autotune_sl = gr.Slider(
                                0.0, 1.0, value=1.0, step=0.05,
                                label="Autotune Strength",
                                visible=False,
                            )
                            autotune_cb.change(
                                fn=toggle_autotune,
                                inputs=autotune_cb,
                                outputs=autotune_sl,
                            )

                    gr.Markdown("**πŸŽ›οΈ Reverb**")
                    reverb_cb = gr.Checkbox(value=False, label="Enable Reverb")
                    with gr.Group(visible=False) as reverb_group:
                        reverb_room_sl = gr.Slider(
                            0.0, 1.0, value=0.15, step=0.05,
                            label="Room Size",
                            info="Larger = bigger sounding space",
                        )
                        reverb_damp_sl = gr.Slider(
                            0.0, 1.0, value=0.7, step=0.05,
                            label="Damping",
                            info="Higher = more absorption, less echo tail",
                        )
                        reverb_wet_sl = gr.Slider(
                            0.0, 1.0, value=0.15, step=0.05,
                            label="Wet Level",
                            info="How much reverb is mixed in (0.15 = subtle)",
                        )
                    reverb_cb.change(
                        fn=lambda v: gr.update(visible=v),
                        inputs=reverb_cb,
                        outputs=reverb_group,
                    )

                    fmt_radio = gr.Radio(
                        choices=["WAV", "MP3", "FLAC", "OPUS"],
                        value="WAV",
                        label="Output Format",
                        info="OPUS = small file (~64 kbps, Telegram/Discord quality)",
                    )
                    convert_btn = gr.Button(
                        "πŸš€ Convert Voice",
                        variant="primary",
                    )

                    gr.Markdown("### 🎧 Output")
                    out_status = gr.Markdown(value="")
                    out_audio = gr.Audio(label="Result (if still on page)", type="filepath", interactive=False)

                    gr.Markdown("#### πŸ” Check Job Status")
                    with gr.Row():
                        job_id_box = gr.Textbox(
                            label="Job ID",
                            placeholder="e.g. a3f2b1c9",
                            scale=3,
                        )
                        poll_btn = gr.Button("πŸ”„ Check", scale=1)
                    poll_status = gr.Markdown(value="")
                    poll_audio = gr.Audio(label="Result", type="filepath", interactive=False)

        # ── TAB 2: Models ─────────────────────────────────────────────────────
        with gr.Tab("πŸ“¦ Models"):
            gr.Markdown("""
            ### Upload a Custom RVC Model
            Provide a **`.zip`** containing:
            - **`model.pth`** β€” weights (required)
            - **`model.index`** β€” FAISS index (optional, improves voice matching)

            **Built-in models** (pre-downloaded on startup):
            Vestia Zeta v1 Β· Vestia Zeta v2 Β· Ayunda Risu Β· Gawr Gura
            """)
            with gr.Row():
                with gr.Column(scale=1):
                    up_zip = gr.File(label="Model ZIP", file_types=[".zip"])
                    up_name = gr.Textbox(
                        label="Model Name",
                        placeholder="Leave blank to use zip filename",
                    )
                    up_btn = gr.Button("πŸ“€ Load Model", variant="primary")
                    up_status = gr.Textbox(label="Status", interactive=False, lines=2)
                with gr.Column(scale=1):
                    gr.Markdown("### Loaded Models")
                    models_table = gr.Dataframe(
                        col_count=(1, "fixed"),
                        value=[[m] for m in initial_models],
                        interactive=False,
                        label="",
                    )
                    refresh_btn = gr.Button("πŸ”„ Refresh")

            up_btn.click(
                fn=upload_model,
                inputs=[up_zip, up_name],
                outputs=[up_status, model_dd, models_table],
            )
            refresh_btn.click(
                fn=refresh_models,
                outputs=[models_table, model_dd],
            )

        # ── TAB 3: Jobs ───────────────────────────────────────────────────────
        with gr.Tab("πŸ“‹ Jobs"):
            gr.Markdown("All submitted jobs, newest first. Click **Refresh** to update.")
            queue_status = gr.Markdown(value=get_queue_info, every=10)
            jobs_table = gr.Dataframe(
                headers=["Job ID", "Model", "Status", "Time", "Download"],
                col_count=(5, "fixed"),
                value=get_jobs_table,
                interactive=False,
                wrap=True,
                datatype=["str", "str", "str", "str", "markdown"],
                every=10,
            )
            refresh_jobs_btn = gr.Button("πŸ”„ Refresh")

            def _refresh_jobs():
                return get_queue_info(), get_jobs_table()

            refresh_jobs_btn.click(fn=_refresh_jobs, outputs=[queue_status, jobs_table])

        # ── TAB 4: Help ───────────────────────────────────────────────────────
        with gr.Tab("ℹ️ Help"):
            gr.Markdown(f"""
            ## How it works
            RVC (Retrieval-Based Voice Conversion) transforms a voice recording to sound
            like a target speaker using only that speaker's model file.

            ---

            ## Quick Guide
            1. Open the **Convert** tab
            2. **Record** via microphone or **upload** an audio file (wav, mp3, flac, ogg …)
            3. Choose a **model** from the dropdown β€” 4 models are pre-loaded on startup
            4. Set **Pitch Shift** if needed (e.g. male β†’ female: try +12 semitones)
            5. Click **πŸš€ Convert Voice** and wait for the result

            ---

            ## Built-in Models
            | Model | Description |
            |---|---|
            | **Vestia Zeta v1** | Hololive ID VTuber, v1 model |
            | **Vestia Zeta v2** | Hololive ID VTuber, v2 model (recommended) |
            | **Ayunda Risu** | Hololive ID VTuber |
            | **Gawr Gura** | Hololive EN VTuber |

            ---

            ## Pitch Extraction Methods
            | Method | Speed | Quality | Best for |
            |---|---|---|---|
            | **rmvpe** | ⚑⚑⚑ | β˜…β˜…β˜…β˜… | General use (default) |
            | **fcpe** | ⚑⚑ | β˜…β˜…β˜…β˜… | Singing |
            | **crepe** | ⚑ | β˜…β˜…β˜…β˜…β˜… | Highest quality, slow |
            | **crepe-tiny** | ⚑⚑ | β˜…β˜…β˜… | Low resource |

            ---

            ## Advanced Settings
            | Setting | Description |
            |---|---|
            | **Index Rate** | Influence of FAISS index on output timbre (0.75 recommended) |
            | **Protect Consonants** | Prevents artefacts on consonants (0.5 = max) |
            | **Respiration Filter Radius** | Smooths pitch curve β€” higher reduces breath noise (0–7, default 3) |
            | **Volume Envelope Mix** | 0.25 = natural blend Β· 1 = preserve input loudness |
            | **Noise Reduction** | Removes background noise before conversion |
            | **Split Long Audio** | Chunks audio for recordings > 60 s |
            | **Autotune** | Snaps pitch to nearest musical note |

            ---

            ## Output Formats
            | Format | Size | Quality |
            |---|---|---|
            | **WAV** | Large | Lossless |
            | **FLAC** | Medium | Lossless compressed |
            | **MP3** | Small | Lossy |
            | **OPUS** | Tiny (~64 kbps) | Telegram/Discord quality |

            ---

            **Device:** `{DEVICE_LABEL}`
            **Max input duration:** {MAX_INPUT_DURATION // 60} minutes

            ---

            ## Credits
            Engine: [Ultimate RVC](https://github.com/JackismyShephard/ultimate-rvc)
            """)

    # Wire convert button after all tabs
    def _submit_and_extract_id(*args):
        import re
        status, audio = submit_job(*args)
        match = re.search(r"[a-f0-9]{8}", status or "")
        job_id = match.group(0) if match else ""
        return status, audio, job_id, get_queue_info(), get_jobs_table()

    convert_btn.click(
        fn=_submit_and_extract_id,
        inputs=[
            inp_mic, inp_file, model_dd,
            pitch_sl, f0_radio,
            index_rate_sl, protect_sl, vol_env_sl,
            clean_cb, clean_sl,
            split_cb, autotune_cb, autotune_sl,
            filter_radius_sl,
            fmt_radio,
            reverb_cb, reverb_room_sl, reverb_damp_sl, reverb_wet_sl,
        ],
        outputs=[out_status, out_audio, job_id_box, queue_status, jobs_table],
    )

    def _poll_and_refresh(job_id):
        status, file = poll_job(job_id)
        return status, file, get_queue_info(), get_jobs_table()

    poll_btn.click(
        fn=_poll_and_refresh,
        inputs=[job_id_box],
        outputs=[poll_status, poll_audio, queue_status, jobs_table],
    )


# ── Launch ────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
    demo.queue(default_concurrency_limit=5)
    demo.launch(
        server_name="0.0.0.0",
        server_port=int(os.getenv("PORT", 7860)),
        max_threads=10,
        ssr_mode=False,
        css=CSS,
    )