Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions CHANGES-FOR-PR-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Voice-Pro Critical Fixes — Wave 2

**Target Repository:** https://github.com/abus-aikorea/voice-pro
**Date:** 2026-04-28
**Status:** Ready for PR review
**Based on:** Real-world testing on Ubuntu 24.04.4 minimal install

---

## 1. What This Wave Fixes

This wave addresses **installation and runtime failures** discovered when testing the clean fixed version on a fresh Ubuntu 24.04.4 system. These are follow-up fixes to the first wave (CHANGES-FOR-PR.md).

---

## 2. Fixes

### Fix 10: Missing `cmake` in System Dependencies (`configure.sh`)

**Problem:** On fresh Ubuntu minimal installs (including 24.04.4), `cmake` is not installed by default. Several pip packages require compilation via CMake during install, causing the build to fail with:
```
CMake Error: CMake was unable to find a build program corresponding to "Unix Makefiles".
CMAKE_MAKE_PROGRAM is not set. You probably need to select a different build tool.
```

**Changes:**
- Added `cmake` to the `apt-get install` line in `configure.sh` alongside `git`, `ffmpeg`, and `build-essential`

**Files changed:** `configure.sh`

---

### Fix 11: ctranslate2 cuDNN Fix — Shell Quoting Bug (`one_click.py`)

**Problem:** The post-install cuDNN 8 library fix in `one_click.py` used an inline `python -c "..."` string with escaped quotes (`\"ctranslate2.libs\"`). When passed through `subprocess.run(cmd, shell=True)`, the shell interpreted the inner quotes and mangled the Python syntax, causing:
```python
SyntaxError: invalid syntax
libs = os.path.join(sp, ctranslate2.libs) if sp else ;
```

**Changes:**
- Replaced the inline `python -c` shell command with a temporary Python script file (`/tmp/fix_cudnn8.py`)
- The script is written to disk and executed directly, completely avoiding shell quoting issues

**Files changed:** `one_click.py`

---

### Fix 12: `torchaudio.AudioMetaData` Missing (`start-voice.py`)

**Problem:** `pyannote.audio` (a dependency of `whisperx`) expects `torchaudio.AudioMetaData` to be available at the top level (`torchaudio.AudioMetaData`). In some torchaudio builds — especially CPU wheels or certain CUDA configurations — this class is only accessible via `torchaudio.backend.common.AudioMetaData` and is not re-exported at the top level. This causes:
```
AttributeError: module 'torchaudio' has no attribute 'AudioMetaData'
```

**Changes:**
- Added a defensive monkey-patch in `start-voice.py` (alongside the existing Gradio 5.x patches)
- If `torchaudio.AudioMetaData` is missing, imports it from `torchaudio.backend.common` and attaches it
- Wrapped in `try/except` so it never crashes if the import path changes in future versions

**Files changed:** `start-voice.py`

---

### Fix 13: Outdated `yt-dlp` Pin (`requirements-voice-*.txt`)

**Problem:** Both `requirements-voice-gpu.txt` and `requirements-voice-cpu.txt` pinned `yt-dlp==2025.11.12`. This version is >5 months old and fails on modern YouTube due to SABR streaming and JS challenge changes.

**Changes:**
- Updated to `yt-dlp>=2026.3.17` in both requirements files

**Files changed:** `requirements-voice-gpu.txt`, `requirements-voice-cpu.txt`

---

## 3. Files Changed (Wave 2)

| File | Fix # | Description |
|---|---|---|
| `configure.sh` | 10 | Add `cmake` to apt dependencies |
| `one_click.py` | 11 | ctranslate2 cuDNN fix → temp script (avoids shell quoting) |
| `start-voice.py` | 12 | Monkey-patch `torchaudio.AudioMetaData` for pyannote.audio |
| `requirements-voice-gpu.txt` | 13 | `yt-dlp>=2026.3.17` |
| `requirements-voice-cpu.txt` | 13 | `yt-dlp>=2026.3.17` |

**Total:** 5 files changed, 0 files removed.

---

## 4. Combined Impact (Wave 1 + Wave 2)

| Metric | Wave 1 | Wave 2 | Total |
|---|---|---|---|
| Fixes | 9 | 4 | **13** |
| Files changed | 24 | 5 | **29** |
| Issues addressed | #76, #62, #60 | — | **3 upstream issues** |

---

## 5. Testing Environment

- **OS:** Ubuntu 24.04.4 LTS (minimal install)
- **Test path:** `./configure.sh` → `./start.sh`
- **Result:** All fixes verified — install completes, app launches, YouTube downloader works

---

## 6. Suggested PR Title for Wave 2

> **fix: cmake dependency, ctranslate2 shell quoting, torchaudio.AudioMetaData, and yt-dlp version**
>
> Follow-up fixes discovered during Ubuntu 24.04.4 testing: adds `cmake` to configure.sh, replaces inline shell script with temp file for ctranslate2 cuDNN fix, patches missing `torchaudio.AudioMetaData` for pyannote.audio compatibility, and updates yt-dlp pin to 2026.3.17+.
178 changes: 178 additions & 0 deletions CHANGES-FOR-PR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Voice-Pro Critical Fixes — Pull Request Summary

**Target Repository:** https://github.com/abus-aikorea/voice-pro
**Date:** 2026-04-27
**Status:** Ready for PR review

---

## 1. What This PR Fixes

This PR addresses **runtime failures** that prevent Voice-Pro from being operational on modern systems. All changes are backward-compatible and additive — the original `./start.sh` launch path works exactly as before.

### Upstream Issues Addressed

| Issue | Title | Fix # | Note |
|---|---|---|---|
| [#76](https://github.com/abus-aikorea/voice-pro/issues/76) | `none_type not iterable` error | 3 | Gradio 5.x `Progress.tqdm()` returns `self` instead of iterable; patched in `start-voice.py` |
| [#62](https://github.com/abus-aikorea/voice-pro/issues/62) | Installation Error | 4 | `setuptools>=80` removed `pkg_resources`; `one_click.py` now pins `<70` and installs whisper without build isolation |
| [#60](https://github.com/abus-aikorea/voice-pro/issues/60) | Installation is too much trouble | 4, 6 | Installer reliability improved; GPU auto-detect removes manual config step |

> Issues **not** addressed by this PR: #61 (Azure SDK TranslatorCredential), #75/#74 (RTX 5070 sm_120 ctranslate2 kernel), #79 (azure-ai-translation-text 1.0.0+), #80 (demucs subprocess failure detection), #68 (missing demucs output files), #67 (Windows grep compatibility).

---

## 2. Critical Fixes

### Fix 1: YouTube Downloader (`app/abus_downloader.py`)

**Problem:** The built-in YouTube downloader fails on most videos because:
- `yt-dlp 2025.11.12` is >5 months old and cannot handle YouTube's current SABR streaming + JS challenges
- No JavaScript runtime is configured (yt-dlp 2026+ only enables Deno by default)
- Metadata is extracted twice per download, doubling HTTP requests and triggering rate limits

**Changes:**
- Updated `ydl_opts` with `js_runtimes: {'node': {}}` and `remote_components: ['ejs:github']`
- Replaced double extraction with single `extract_info(..., download=False)` + `process_ie_result(info, download=True)`

**Note:** `yt-dlp` itself must also be updated to `2026.3.17+` in the environment (done via pip in the conda env).

---

### Fix 2: VAD Model Path (`src/vad.py`)

**Problem:** The VAD constructor hardcodes `~/.cache/whisper-live/silero_vad.onnx` and ignores the project-local copy at `model/vad/silero_vad.onnx`. On fresh clones or systems without the cache, VAD fails immediately.

**Changes:**
- Constructor now checks `model/vad/silero_vad.onnx` (project-local) first
- Falls back to `~/.cache/whisper-live/silero_vad.onnx`
- Prints clear manual-sourcing instructions if neither is found

---

### Fix 3: Gradio 5.x Compatibility (`app/abus_path.py`, `app/gradio_*.py`, `start-voice.py`)

**Problem:** Gradio 5.x introduced breaking API changes:
- `gr.Progress.tqdm()` returns `self` instead of a wrapped iterable, causing `"NoneType object is not iterable"` errors (reported in upstream issue #76)
- `gr.File(type='filepath')` now returns a `NamedString` (str subclass with `.name`) instead of a plain str, while `gr.Audio(type='filepath')` still returns a plain str. Code using `file_obj.name` crashes when the input is a plain str.
- `FileData.model_validate()` rejects payloads with missing/None `meta` fields, causing upload failures.

**Changes:**
- Added `gradio_file_path(file_obj)` helper in `app/abus_path.py` that handles `str`, `NamedString`, and legacy file objects
- Updated **13 `app/gradio_*.py` files** to use `gradio_file_path(file_obj)` instead of `file_obj.name`
- Added monkey-patches in `start-voice.py` for `Progress.tqdm` (wraps with standard `tqdm`) and `FileData.model_validate` (injects default `meta` when missing)

**Files affected:**
`app/abus_path.py`, `app/gradio_aicover.py`, `app/gradio_asr.py`, `app/gradio_demixing.py`, `app/gradio_gulliver.py`, `app/gradio_kara.py`, `app/gradio_rvc.py`, `app/gradio_translate.py`, `app/gradio_tts_cosyvoice.py`, `app/gradio_tts_edge.py`, `app/gradio_tts_f5.py`, `app/gradio_tts_kokoro.py`, `app/gradio_tts_rvc.py`, `app/gradio_vsr.py`, `start-voice.py`

---

### Fix 4: Installer Build Fixes (`one_click.py`)

**Problem:** Fresh installs fail on modern Python environments because:
- `setuptools>=80` removed `pkg_resources`, breaking `openai-whisper` (sdist that imports `pkg_resources` during build)
- `openai-whisper` build isolation pulls in a newer setuptools that lacks `pkg_resources`
- `ctranslate2` bundles a renamed cuDNN 8 library but omits the split `.so` files (e.g., `libcudnn_ops_infer.so.8`) that it `dlopen`s at runtime, causing CUDA errors
- The inline `python -c` shell command for the ctranslate2 fix had quoting issues that caused `SyntaxError` on some shells

**Changes:**
- Pre-install `setuptools<70` and `wheel` before requirements
- Install `openai-whisper==20240930` with `--no-build-isolation`
- Post-install: write the cuDNN fix to a temp Python script file instead of an inline `python -c` string, avoiding shell quoting bugs

---

### Fix 5: cuDNN Version Bump (`requirements-voice-gpu.txt`)

**Problem:** `nvidia-cudnn-cu12==8.9.7.29` pins an old version that may conflict with newer PyTorch builds.

**Changes:**
- Relaxed to `nvidia-cudnn-cu12>=9.1.0.70`

---

### Fix 6: GPU Auto-Detection (`start.sh`, `update.sh`)

**Problem:** Users on fresh systems often forget to set `GPU_CHOICE`, causing the app to default to an incorrect mode or prompt interactively.

**Changes:**
- Added auto-detection: if `nvidia-smi` is available, set `GPU_CHOICE=G`; otherwise `GPU_CHOICE=C`

---

### Fix 7: Build Tool Dependencies (`configure.sh`)

**Problem:** Ubuntu minimal installs (and some fresh 24.04 setups) lack `cmake`, causing pip packages that require compilation to fail with `CMAKE_MAKE_PROGRAM is not set`.

**Changes:**
- Added `cmake` to the `apt-get install` line alongside `git`, `ffmpeg`, and `build-essential`

---

### Fix 8: torchaudio / pyannote.audio Compatibility (`start-voice.py`)

**Problem:** `pyannote.audio` (dependency of `whisperx`) expects `torchaudio.AudioMetaData` to be available at the top level. In some torchaudio builds (especially CPU wheels or certain CUDA builds), this class is not re-exported, causing `AttributeError: module 'torchaudio' has no attribute 'AudioMetaData'` on import.

**Changes:**
- Added monkey-patch in `start-voice.py`: if `torchaudio.AudioMetaData` is missing, import it from `torchaudio.backend.common` and attach it

---

### Fix 9: yt-dlp Version Pin (`requirements-voice-*.txt`)

**Problem:** `yt-dlp==2025.11.12` is too old for modern YouTube.

**Changes:**
- Relaxed to `yt-dlp>=2026.3.17` in both GPU and CPU requirements files

---

## 3. Files Changed

| File | Fix # | Description |
|---|---|---|
| `app/abus_downloader.py` | 1 | YouTube JS runtime + single extraction |
| `src/vad.py` | 2 | Local VAD path check first |
| `app/abus_path.py` | 3 | `gradio_file_path()` helper |
| `app/gradio_aicover.py` | 3 | Use `gradio_file_path()` |
| `app/gradio_asr.py` | 3 | Use `gradio_file_path()` |
| `app/gradio_demixing.py` | 3 | Use `gradio_file_path()` |
| `app/gradio_gulliver.py` | 3 | Use `gradio_file_path()` |
| `app/gradio_kara.py` | 3 | Use `gradio_file_path()` |
| `app/gradio_rvc.py` | 3 | Use `gradio_file_path()` |
| `app/gradio_translate.py` | 3 | Use `gradio_file_path()` |
| `app/gradio_tts_cosyvoice.py` | 3 | Use `gradio_file_path()` |
| `app/gradio_tts_edge.py` | 3 | Use `gradio_file_path()` |
| `app/gradio_tts_f5.py` | 3 | Use `gradio_file_path()` |
| `app/gradio_tts_kokoro.py` | 3 | Use `gradio_file_path()` |
| `app/gradio_tts_rvc.py` | 3 | Use `gradio_file_path()` |
| `app/gradio_vsr.py` | 3 | Use `gradio_file_path()` |
| `start-voice.py` | 3, 8 | Monkey-patches for Gradio 5.x tqdm + FileData + torchaudio.AudioMetaData |
| `one_click.py` | 4 | setuptools, whisper, ctranslate2 cuDNN fixes (temp script) |
| `start.sh` | 6 | GPU auto-detect |
| `update.sh` | 6 | GPU auto-detect |
| `configure.sh` | 7 | Add `cmake` to apt dependencies |
| `requirements-voice-gpu.txt` | 5, 9 | cuDNN version bump + yt-dlp update |
| `requirements-voice-cpu.txt` | 9 | yt-dlp update |

**Total:** 24 files changed, 0 files removed.

---

## 4. Testing

| Test | Result |
|---|---|
| YouTube download (modern video with JS challenges) | ✅ Success |
| VAD initialization (fresh clone, no cache) | ✅ Success |
| Gradio file upload (audio + video inputs) | ✅ Success |
| Progress bar iteration (subtitle generation) | ✅ Success |
| Fresh install on Linux Mint 22.2 | ✅ Success |

---

## 5. Suggested PR Title

> **fix: YouTube downloader, VAD paths, Gradio 5.x compatibility, and installer build errors**
>
> Fixes yt-dlp JS runtime config for modern YouTube, adds project-local VAD fallback, resolves Gradio 5.x breaking changes (tqdm/FileData/file paths), and patches one_click.py for setuptools/pkg_resources and ctranslate2 cuDNN compatibility.
3 changes: 2 additions & 1 deletion app/abus_demucs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import subprocess
import sys

from app.abus_ffmpeg import *
from app.abus_path import *
Expand Down Expand Up @@ -28,7 +29,7 @@ def demucs_split_file(input_path: str, output_dir, demucs_model: str, audio_form
demucs_inst_file = os.path.join(temp_directory, demucs_model, file_name, "no_vocals.wav")
demucs_vocal_file = os.path.join(temp_directory, demucs_model, file_name, "vocals.wav")

command = f'python -m demucs.separate -n {demucs_model} --two-stems=vocals "{input_path}" -o "{temp_directory}" {output_option}'
command = f'{sys.executable} -m demucs.separate -n {demucs_model} --two-stems=vocals "{input_path}" -o "{temp_directory}" {output_option}'
command += f' --repo model/demucs'
logger.debug(f'[abus:demucs_split_file] {command}')

Expand Down
12 changes: 10 additions & 2 deletions app/abus_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ def yt_download(self, url: str, download_folder: str, quality: str = "good", max
ydl_opts['progress_hooks'] = [self.dl_progress_hook]
ydl_opts['playlist_items'] = '1'

# Enable Node.js JS runtime for YouTube challenge solving
# (yt-dlp 2026+ only enables Deno by default)
ydl_opts['js_runtimes'] = {'node': {}}
ydl_opts['remote_components'] = ['ejs:github']

# User Agent 설정 추가
ydl_opts['http_headers'] = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
Expand All @@ -87,8 +92,11 @@ def yt_download(self, url: str, download_folder: str, quality: str = "good", max

filename_collector = FilenameCollectorPP()
with YoutubeDL(ydl_opts) as ydl:
# Extract metadata once, then download from the same result
# to avoid redundant HTTP requests and rate-limiting
info = ydl.extract_info(url, download=False)

if maxDuration and maxDuration > 0:
info = ydl.extract_info(url, download=False)
entries = "entries" in info and info["entries"] or [info]
total_duration = 0

Expand All @@ -100,7 +108,7 @@ def yt_download(self, url: str, download_folder: str, quality: str = "good", max
raise ExceededMaximumDuration(videoDuration=total_duration, maxDuration=maxDuration, message="Video is too long")

ydl.add_post_processor(filename_collector)
ydl.download([url])
ydl.process_ie_result(info, download=True)

if len(filename_collector.filenames) <= 0:
raise Exception("Cannot download " + url)
Expand Down
16 changes: 16 additions & 0 deletions app/abus_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,4 +435,20 @@ def cmd_select_folder(default_path='.'):
logger.error(f"[abus_path.py] cmd_select_folder - Error: {e}")
sys.stderr.flush()
return default_path


def gradio_file_path(file_obj):
"""Safely extract a file path from a Gradio file component input.

In Gradio 4/5, gr.File(type='filepath') returns a NamedString (str subclass
with .name). gr.Audio(type='filepath') returns a plain str. This helper
handles all cases robustly, including legacy file-like objects.
"""
if file_obj is None:
return None
if isinstance(file_obj, str):
return file_obj
if hasattr(file_obj, 'name'):
return file_obj.name
return str(file_obj)

2 changes: 1 addition & 1 deletion app/gradio_aicover.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def upload_source(self,
def _upload(self,
file_obj, mic_file, youtube_url: str, video_quality: str, audio_format: str):
if (file_obj is not None):
uploaded_file = cmd_copy_file_to(file_obj.name, path_workspace_subfolder(file_obj.name))
uploaded_file = cmd_copy_file_to(gradio_file_path(file_obj), path_workspace_subfolder(gradio_file_path(file_obj)))
elif mic_file and mic_file.strip():
uploaded_file = cmd_copy_file_to(mic_file, path_workspace_subfolder(mic_file))
elif youtube_url and youtube_url.strip():
Expand Down
2 changes: 1 addition & 1 deletion app/gradio_asr.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def upload_source(self,
def _upload(self,
file_obj, mic_file, youtube_url: str, video_quality: str, audio_format: str):
if (file_obj is not None):
uploaded_file = cmd_copy_file_to(file_obj.name, path_workspace_subfolder(file_obj.name))
uploaded_file = cmd_copy_file_to(gradio_file_path(file_obj), path_workspace_subfolder(gradio_file_path(file_obj)))
elif mic_file and mic_file.strip():
uploaded_file = cmd_copy_file_to(mic_file, path_workspace_subfolder(mic_file))
elif youtube_url and youtube_url.strip():
Expand Down
2 changes: 1 addition & 1 deletion app/gradio_demixing.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def upload_source(self,
def _upload(self,
file_obj, mic_file, youtube_url: str, video_quality: str, audio_format: str):
if (file_obj is not None):
uploaded_file = cmd_copy_file_to(file_obj.name, path_workspace_subfolder(file_obj.name))
uploaded_file = cmd_copy_file_to(gradio_file_path(file_obj), path_workspace_subfolder(gradio_file_path(file_obj)))
elif mic_file and mic_file.strip():
uploaded_file = cmd_copy_file_to(mic_file, path_workspace_subfolder(mic_file))
elif youtube_url and youtube_url.strip():
Expand Down
2 changes: 1 addition & 1 deletion app/gradio_gulliver.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def gradio_upload_source(self,
def _upload(self,
file_obj, mic_file, youtube_url: str, video_quality: str, audio_format: str):
if (file_obj is not None):
uploaded_file = cmd_copy_file_to(file_obj.name, path_workspace_subfolder(file_obj.name))
uploaded_file = cmd_copy_file_to(gradio_file_path(file_obj), path_workspace_subfolder(gradio_file_path(file_obj)))
elif mic_file and mic_file.strip():
uploaded_file = cmd_copy_file_to(mic_file, path_workspace_subfolder(mic_file))
elif youtube_url and youtube_url.strip():
Expand Down
Loading