mirror of
https://github.com/remsky/Kokoro-FastAPI.git
synced 2025-08-05 16:48:53 +00:00
Remove voice manager tests and update Dockerfiles for improved dependency management and user permissions
This commit is contained in:
parent
a026d4d5ee
commit
165ffccd01
3 changed files with 28 additions and 153 deletions
|
@ -1,134 +0,0 @@
|
||||||
import pytest
|
|
||||||
from unittest.mock import AsyncMock, patch, MagicMock
|
|
||||||
import torch
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from ..src.inference.voice_manager import VoiceManager
|
|
||||||
from ..src.structures.model_schemas import VoiceConfig
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_voice_tensor():
|
|
||||||
return torch.randn(10, 10) # Dummy tensor
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def voice_manager():
|
|
||||||
return VoiceManager(VoiceConfig())
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_load_voice(voice_manager, mock_voice_tensor):
|
|
||||||
"""Test loading a single voice"""
|
|
||||||
with patch("api.src.core.paths.load_voice_tensor", new_callable=AsyncMock) as mock_load:
|
|
||||||
mock_load.return_value = mock_voice_tensor
|
|
||||||
with patch("os.path.exists", return_value=True):
|
|
||||||
voice = await voice_manager.load_voice("af_bella", "cpu")
|
|
||||||
assert torch.equal(voice, mock_voice_tensor)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_load_voice_not_found(voice_manager):
|
|
||||||
"""Test loading non-existent voice"""
|
|
||||||
with patch("os.path.exists", return_value=False):
|
|
||||||
with pytest.raises(RuntimeError, match="Voice not found: invalid_voice"):
|
|
||||||
await voice_manager.load_voice("invalid_voice", "cpu")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Local saving is optional and not critical to functionality")
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_combine_voices_with_saving(voice_manager, mock_voice_tensor):
|
|
||||||
"""Test combining voices with local saving enabled"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_combine_voices_without_saving(voice_manager, mock_voice_tensor):
|
|
||||||
"""Test combining voices without local saving"""
|
|
||||||
with patch("api.src.core.paths.load_voice_tensor", new_callable=AsyncMock) as mock_load, \
|
|
||||||
patch("torch.save") as mock_save, \
|
|
||||||
patch("os.makedirs"), \
|
|
||||||
patch("os.path.exists", return_value=True):
|
|
||||||
|
|
||||||
# Setup mocks
|
|
||||||
mock_load.return_value = mock_voice_tensor
|
|
||||||
|
|
||||||
# Mock settings
|
|
||||||
with patch("api.src.core.config.settings") as mock_settings:
|
|
||||||
mock_settings.allow_local_voice_saving = False
|
|
||||||
mock_settings.voices_dir = "/mock/voices"
|
|
||||||
|
|
||||||
# Combine voices
|
|
||||||
combined = await voice_manager.combine_voices(["af_bella", "af_sarah"], "cpu")
|
|
||||||
assert combined == "af_bella+af_sarah" # Note: using + separator
|
|
||||||
|
|
||||||
# Verify voice was not saved
|
|
||||||
mock_save.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_combine_voices_single_voice(voice_manager):
|
|
||||||
"""Test combining with single voice"""
|
|
||||||
with pytest.raises(ValueError, match="At least 2 voices are required"):
|
|
||||||
await voice_manager.combine_voices(["af_bella"], "cpu")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_list_voices(voice_manager):
|
|
||||||
"""Test listing available voices"""
|
|
||||||
with patch("os.listdir", return_value=["af_bella.pt", "af_sarah.pt", "af_bella+af_sarah.pt"]), \
|
|
||||||
patch("os.makedirs"):
|
|
||||||
voices = await voice_manager.list_voices()
|
|
||||||
assert len(voices) == 3
|
|
||||||
assert "af_bella" in voices
|
|
||||||
assert "af_sarah" in voices
|
|
||||||
assert "af_bella+af_sarah" in voices
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_load_combined_voice(voice_manager, mock_voice_tensor):
|
|
||||||
"""Test loading a combined voice"""
|
|
||||||
with patch("api.src.core.paths.load_voice_tensor", new_callable=AsyncMock) as mock_load:
|
|
||||||
mock_load.return_value = mock_voice_tensor
|
|
||||||
with patch("os.path.exists", return_value=True):
|
|
||||||
voice = await voice_manager.load_voice("af_bella+af_sarah", "cpu")
|
|
||||||
assert torch.equal(voice, mock_voice_tensor)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cache_management(mock_voice_tensor):
|
|
||||||
"""Test voice cache management"""
|
|
||||||
# Create voice manager with small cache size
|
|
||||||
config = VoiceConfig(cache_size=2)
|
|
||||||
voice_manager = VoiceManager(config)
|
|
||||||
|
|
||||||
# Add items to cache
|
|
||||||
voice_manager._voice_cache = {
|
|
||||||
"voice1_cpu": torch.randn(5, 5),
|
|
||||||
"voice2_cpu": torch.randn(5, 5),
|
|
||||||
"voice3_cpu": torch.randn(5, 5), # Add one more than cache size
|
|
||||||
}
|
|
||||||
|
|
||||||
# Try managing cache
|
|
||||||
voice_manager._manage_cache()
|
|
||||||
|
|
||||||
# Check cache size maintained
|
|
||||||
assert len(voice_manager._voice_cache) <= 2
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_voice_loading_with_cache(voice_manager, mock_voice_tensor):
|
|
||||||
"""Test voice loading with cache enabled"""
|
|
||||||
with patch("api.src.core.paths.load_voice_tensor", new_callable=AsyncMock) as mock_load, \
|
|
||||||
patch("os.path.exists", return_value=True):
|
|
||||||
|
|
||||||
mock_load.return_value = mock_voice_tensor
|
|
||||||
|
|
||||||
# First load should hit disk
|
|
||||||
voice1 = await voice_manager.load_voice("af_bella", "cpu")
|
|
||||||
assert mock_load.call_count == 1
|
|
||||||
|
|
||||||
# Second load should hit cache
|
|
||||||
voice2 = await voice_manager.load_voice("af_bella", "cpu")
|
|
||||||
assert mock_load.call_count == 1 # Still 1
|
|
||||||
|
|
||||||
assert torch.equal(voice1, voice2)
|
|
|
@ -1,12 +1,14 @@
|
||||||
FROM --platform=$BUILDPLATFORM python:3.10-slim
|
FROM --platform=$BUILDPLATFORM python:3.10-slim
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies and check espeak location
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y \
|
||||||
espeak-ng \
|
espeak-ng \
|
||||||
git \
|
git \
|
||||||
libsndfile1 \
|
libsndfile1 \
|
||||||
curl \
|
curl \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
|
&& dpkg -L espeak-ng \
|
||||||
|
&& find / -name "espeak-ng-data" \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
@ -15,12 +17,12 @@ RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
||||||
mv /root/.local/bin/uv /usr/local/bin/ && \
|
mv /root/.local/bin/uv /usr/local/bin/ && \
|
||||||
mv /root/.local/bin/uvx /usr/local/bin/
|
mv /root/.local/bin/uvx /usr/local/bin/
|
||||||
|
|
||||||
# Create non-root user
|
# Create non-root user and set up directories and permissions
|
||||||
RUN useradd -m -u 1000 appuser
|
RUN useradd -m -u 1000 appuser && \
|
||||||
|
mkdir -p /app/api/src/models/v1_0 && \
|
||||||
|
chown -R appuser:appuser /app && \
|
||||||
|
chown -R appuser:appuser /lib/x86_64-linux-gnu/espeak-ng-data
|
||||||
|
|
||||||
# Create directories and set ownership
|
|
||||||
RUN mkdir -p /app/api/src/models/v1_0 && \
|
|
||||||
chown -R appuser:appuser /app
|
|
||||||
|
|
||||||
USER appuser
|
USER appuser
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
@ -49,7 +51,6 @@ ENV PYTHONUNBUFFERED=1 \
|
||||||
UV_LINK_MODE=copy \
|
UV_LINK_MODE=copy \
|
||||||
USE_GPU=false
|
USE_GPU=false
|
||||||
|
|
||||||
# Core settings that differ from config.py defaults
|
|
||||||
ENV DOWNLOAD_MODEL=true
|
ENV DOWNLOAD_MODEL=true
|
||||||
# Download model if enabled
|
# Download model if enabled
|
||||||
RUN if [ "$DOWNLOAD_MODEL" = "true" ]; then \
|
RUN if [ "$DOWNLOAD_MODEL" = "true" ]; then \
|
||||||
|
@ -57,4 +58,4 @@ RUN if [ "$DOWNLOAD_MODEL" = "true" ]; then \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run FastAPI server
|
# Run FastAPI server
|
||||||
CMD ["uv", "run", "python", "-m", "uvicorn", "api.src.main:app", "--host", "0.0.0.0", "--port", "8880", "--log-level", "debug"]
|
CMD ["uv", "run", "python", "-m", "uvicorn", "api.src.main:app", "--host", "0.0.0.0", "--port", "8880", "--log-level", "debug"]
|
|
@ -1,9 +1,10 @@
|
||||||
FROM --platform=$BUILDPLATFORM nvidia/cuda:12.1.0-runtime-ubuntu22.04
|
FROM --platform=$BUILDPLATFORM nvidia/cuda:12.1.0-cudnn8-runtime-ubuntu22.04
|
||||||
|
|
||||||
# Set non-interactive frontend
|
# Set non-interactive frontend
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
# Install Python and other dependencies
|
# Install Python and other dependencies
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3.10 \
|
python3.10 \
|
||||||
python3.10-venv \
|
python3.10-venv \
|
||||||
espeak-ng \
|
espeak-ng \
|
||||||
|
@ -11,19 +12,26 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
libsndfile1 \
|
libsndfile1 \
|
||||||
curl \
|
curl \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
&& ls -la /usr/lib/x86_64-linux-gnu/espeak-ng-data \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install UV using the installer script
|
# Create user and set up permissions
|
||||||
|
RUN useradd -m -u 1000 appuser && \
|
||||||
|
mkdir -p /app/api/src/models/v1_0 && \
|
||||||
|
chown -R appuser:appuser /app && \
|
||||||
|
chown -R appuser:appuser /usr/lib/x86_64-linux-gnu/espeak-ng-data
|
||||||
|
|
||||||
|
|
||||||
|
# Rest of your Dockerfile...
|
||||||
|
|
||||||
|
# Install UV in a separate step
|
||||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
||||||
mv /root/.local/bin/uv /usr/local/bin/ && \
|
mv /root/.local/bin/uv /usr/local/bin/ && \
|
||||||
mv /root/.local/bin/uvx /usr/local/bin/ && \
|
mv /root/.local/bin/uvx /usr/local/bin/
|
||||||
useradd -m -u 1000 appuser && \
|
|
||||||
mkdir -p /app/api/src/models/v1_0 && \
|
|
||||||
chown -R appuser:appuser /app
|
|
||||||
|
|
||||||
USER appuser
|
USER appuser
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy dependency files
|
# Copy dependency files
|
||||||
COPY --chown=appuser:appuser pyproject.toml ./pyproject.toml
|
COPY --chown=appuser:appuser pyproject.toml ./pyproject.toml
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue