Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__/
*.py[cod]
.venv/
152 changes: 151 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,151 @@
# repka
# Neural Realtime Voice Changer (MVP)

MVP-проект на Python/PyTorch для изменения голоса в реальном времени с обучением на ваших аудио-сэмплах.

## Что умеет

- обучать модель конвертации голоса на папках с WAV/FLAC/OGG;
- строить профиль целевого голоса (`.pt`) по набору сэмплов;
- менять голос в реальном времени с микрофона;
- конвертировать файл офлайн (для проверки качества).

## Ограничения MVP

- это не коммерческий прод-уровень, а рабочий прототип;
- качество сильно зависит от датасета (чистота записи, длительность, разнообразие);
- для real-time нужен стабильный аудио-драйвер и желательно GPU.

## Установка

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -r requirements.txt
```

## AMD RX 470 8GB (Polaris): важные настройки

Для RX 470 поддержка в новых ROCm-сборках часто нестабильна, поэтому добавлен флаг:

- `--amd-gfx-version 8.0.3` (устанавливает `HSA_OVERRIDE_GFX_VERSION=8.0.3`)

Дополнительно для старых AMD лучше отключать mixed precision:

- `--amp-mode off`

Если у вас уже настроен ROCm/PyTorch ROCm, запускайте скрипты с этими параметрами.
Если GPU не подхватился, временно используйте `--device cpu`.

## Подготовка датасета

Структура:

```text
data/
speaker_1/
a.wav
b.wav
speaker_2/
c.wav
d.wav
...
```

Рекомендации:

- минимум 2 спикера для нормальной межголосовой конвертации;
- от 5-15 минут чистой речи на каждого;
- одинаковая частота дискретизации не обязательна (скрипт ресемплит в 16k).

## Обучение

```bash
python3 scripts/train_voice_converter.py \
--data-dir data \
--output-dir artifacts \
--epochs 40 \
--batch-size 12 \
--device auto \
--amp-mode auto
```

Чекпоинты: `artifacts/checkpoints/latest.pt`, `artifacts/checkpoints/epoch_XXX.pt`.

Для RX 470 (более безопасный старт):

```bash
python3 scripts/train_voice_converter.py \
--data-dir data \
--output-dir artifacts_rx470 \
--epochs 40 \
--batch-size 4 \
--device cuda \
--amd-gfx-version 8.0.3 \
--amp-mode off
```

## Дообучение на новых сэмплах

Добавьте новые папки в `data/` и запустите:

```bash
python3 scripts/train_voice_converter.py \
--data-dir data \
--output-dir artifacts_finetune \
--resume-checkpoint artifacts/checkpoints/latest.pt \
--epochs 15 \
--device auto \
--amp-mode auto
```

## Создание профиля целевого голоса

Можно передать папку или один файл:

```bash
python3 scripts/build_voice_profile.py \
--checkpoint artifacts/checkpoints/latest.pt \
--samples samples/target_voice \
--output-profile artifacts/profiles/target_profile.pt \
--device auto \
--amd-gfx-version 8.0.3
```

## Запуск в реальном времени (микрофон -> динамики)

Сначала можно посмотреть устройства:

```bash
python3 scripts/realtime_voice_changer.py --list-devices
```

Запуск:

```bash
python3 scripts/realtime_voice_changer.py \
--checkpoint artifacts/checkpoints/latest.pt \
--profile artifacts/profiles/target_profile.pt \
--block-size 1024 \
--device auto \
--amd-gfx-version 8.0.3
```

Чем меньше `--block-size`, тем ниже задержка, но выше риск артефактов.

## Офлайн конвертация файла

```bash
python3 scripts/convert_file.py \
--checkpoint artifacts/checkpoints/latest.pt \
--profile artifacts/profiles/target_profile.pt \
--input demo/input.wav \
--output demo/output.wav \
--device auto \
--amd-gfx-version 8.0.3
```

## Важные замечания

- Используйте эту систему только с согласия владельца голоса.
- Для стрима в Discord/OBS обычно удобнее использовать виртуальный аудио-кабель.
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
torch
torchaudio
numpy
tqdm
sounddevice
82 changes: 82 additions & 0 deletions scripts/build_voice_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Build target voice profile embedding from sample audios."""

from __future__ import annotations

import argparse
from pathlib import Path
import sys

PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Create speaker profile from voice samples.")
parser.add_argument(
"--checkpoint",
required=True,
help="Path to trained checkpoint (*.pt)",
)
parser.add_argument(
"--samples",
required=True,
help="Directory or file with target speaker samples",
)
parser.add_argument(
"--output-profile",
default="artifacts/profiles/target_profile.pt",
help="Path to output profile .pt file",
)
parser.add_argument("--segment-frames", type=int, default=96)
parser.add_argument("--max-segments-per-file", type=int, default=8)
parser.add_argument(
"--device",
default="auto",
help='Device: "auto", "cpu", "cuda", "cuda:0"...',
)
parser.add_argument(
"--amd-gfx-version",
default=None,
help="Sets HSA_OVERRIDE_GFX_VERSION before torch import (RX 470: 8.0.3).",
)
return parser.parse_args()


def main() -> None:
args = parse_args()
from voice_changer.runtime import configure_amd_runtime

applied_gfx_override = configure_amd_runtime(args.amd_gfx_version)
if applied_gfx_override:
print(f"HSA_OVERRIDE_GFX_VERSION={applied_gfx_override}")

from voice_changer.inference import (
build_voice_profile_embedding,
collect_audio_files,
load_inference_bundle,
save_voice_profile,
)

bundle = load_inference_bundle(args.checkpoint, device=args.device)
sample_files = collect_audio_files(args.samples)
embedding = build_voice_profile_embedding(
bundle=bundle,
sample_paths=sample_files,
segment_frames=args.segment_frames,
max_segments_per_file=args.max_segments_per_file,
)
save_voice_profile(
output_path=args.output_profile,
embedding=embedding,
source_files=sample_files,
sample_rate=bundle.processor.sample_rate,
checkpoint_path=str(Path(args.checkpoint).resolve()),
)
print(f"Saved profile: {Path(args.output_profile).resolve()}")
print(f"Used files: {len(sample_files)}")


if __name__ == "__main__":
main()
59 changes: 59 additions & 0 deletions scripts/convert_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Convert a WAV/FLAC/OGG file with trained voice changer."""

from __future__ import annotations

import argparse
from pathlib import Path
import sys

PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Offline file conversion with target profile.")
parser.add_argument("--checkpoint", required=True)
parser.add_argument("--profile", required=True)
parser.add_argument("--input", required=True, help="Input audio file")
parser.add_argument("--output", required=True, help="Output audio file")
parser.add_argument("--chunk-frames", type=int, default=128)
parser.add_argument(
"--device",
default="auto",
help='Torch device: "auto", "cpu", "cuda"...',
)
parser.add_argument(
"--amd-gfx-version",
default=None,
help="Sets HSA_OVERRIDE_GFX_VERSION before torch import (RX 470: 8.0.3).",
)
return parser.parse_args()


def main() -> None:
args = parse_args()
from voice_changer.runtime import configure_amd_runtime

applied_gfx_override = configure_amd_runtime(args.amd_gfx_version)
if applied_gfx_override:
print(f"HSA_OVERRIDE_GFX_VERSION={applied_gfx_override}")

from voice_changer.convert import convert_file
from voice_changer.inference import load_inference_bundle, load_voice_profile

bundle = load_inference_bundle(args.checkpoint, device=args.device)
target_embedding = load_voice_profile(args.profile, device=bundle.device)
convert_file(
bundle=bundle,
input_path=args.input,
output_path=args.output,
target_embedding=target_embedding,
chunk_frames=args.chunk_frames,
)
print(f"Converted audio saved to: {Path(args.output).resolve()}")


if __name__ == "__main__":
main()
Loading