Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
fail-fast: false

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5

# Match Dockerfile dependencies
- name: Install Dependencies
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
fi

- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
ref: ${{ steps.resolve-ref.outputs.source_ref }}
fetch-depth: 0
Expand Down Expand Up @@ -100,7 +100,7 @@ jobs:
runs-on: ${{ matrix.runs_on }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
ref: ${{ needs.prepare-release.outputs.source_ref }}

Expand All @@ -115,14 +115,14 @@ jobs:
df -h

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 # Use v3
uses: docker/setup-buildx-action@v4
with:
driver-opts: |
image=moby/buildkit:v0.21.1
network=host

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3 # Use v3
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
Expand Down Expand Up @@ -276,7 +276,7 @@ jobs:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ jobs:
runs-on: ${{ matrix.runs_on }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
ref: ${{ inputs.branch_name }}

Expand All @@ -88,7 +88,7 @@ jobs:
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
with:
driver-opts: image=moby/buildkit:v0.21.1

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test_client_image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
OWNER: ${{ vars.OWNER || 'remsky' }}
IMAGE_NAME: ${{ vars.TEST_CLIENT_IMAGE_NAME || 'tts-api-test-client' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5

- name: Resolve image refs
id: refs
Expand All @@ -61,7 +61,7 @@ jobs:
echo "latest=${BASE}:latest" >> "$GITHUB_OUTPUT"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

- name: Log in to GHCR
if: github.event_name != 'workflow_dispatch' || inputs.push
Expand Down
5 changes: 4 additions & 1 deletion api/src/core/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from importlib.metadata import PackageNotFoundError, version as _pkg_version
from importlib.metadata import (
PackageNotFoundError,
version as _pkg_version,
)
from pathlib import Path

import torch
Expand Down
3 changes: 2 additions & 1 deletion api/src/routers/development.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ async def dual_output():
# Add any chunks that may be in the acumulator into the return word_timestamps
if chunk_data.word_timestamps is not None:
chunk_data.word_timestamps = (
timestamp_acumulator + chunk_data.word_timestamps
timestamp_acumulator
+ chunk_data.word_timestamps
)
timestamp_acumulator = []
else:
Expand Down
4 changes: 2 additions & 2 deletions api/src/routers/web_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ async def get_web_config():
"""Get web player configuration including UVICORN_ROOT_PATH."""
if not settings.enable_web_player:
raise HTTPException(status_code=404, detail="Web player is disabled")

root_path = os.environ.get("UVICORN_ROOT_PATH", "")

return {
"root_path": root_path,
"version": settings.api_version,
Expand Down
12 changes: 7 additions & 5 deletions api/src/services/streaming_audio_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,29 @@ def __init__(self, format: str, sample_rate: int, channels: int = 1):
self.output_buffer = BytesIO()
container_options = {}
# Try disabling Xing VBR header for MP3 to fix iOS timeline reading issues
if self.format == 'mp3':
if self.format == "mp3":
# Disable Xing VBR header
container_options = {'write_xing': '0'}
container_options = {"write_xing": "0"}
logger.debug("Disabling Xing VBR header for MP3 encoding.")

self.container = av.open(
self.output_buffer,
mode="w",
format=self.format if self.format != "aac" else "adts",
options=container_options # Pass options here
options=container_options, # Pass options here
)
self.stream = self.container.add_stream(
codec_map[self.format],
rate=self.sample_rate,
layout="mono" if self.channels == 1 else "stereo",
)
# Set bit_rate only for codecs where it's applicable and useful
if self.format in ['mp3', 'aac', 'opus']:
if self.format in ["mp3", "aac", "opus"]:
self.stream.bit_rate = 128000
else:
raise ValueError(f"Unsupported format: {self.format}") # Use self.format here
raise ValueError(
f"Unsupported format: {self.format}"
) # Use self.format here

def close(self):
if hasattr(self, "container"):
Expand Down
37 changes: 19 additions & 18 deletions api/src/services/text_processing/normalizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import inflect
from numpy import number

# from text_to_num import text2num
from torch import mul

Expand Down Expand Up @@ -135,20 +136,20 @@
}

SYMBOL_REPLACEMENTS = {
'~': ' ',
'@': ' at ',
'#': ' number ',
'$': ' dollar ',
'%': ' percent ',
'^': ' ',
'&': ' and ',
'*': ' ',
'_': ' ',
'|': ' ',
'\\': ' ',
'/': ' slash ',
'=': ' equals ',
'+': ' plus ',
"~": " ",
"@": " at ",
"#": " number ",
"$": " dollar ",
"%": " percent ",
"^": " ",
"&": " and ",
"*": " ",
"_": " ",
"|": " ",
"\\": " ",
"/": " slash ",
"=": " equals ",
"+": " plus ",
}

MONEY_UNITS = {"$": ("dollar", "cent"), "£": ("pound", "pence"), "€": ("euro", "cent")}
Expand Down Expand Up @@ -408,7 +409,7 @@ def handle_time(t: re.Match[str]) -> str:

def normalize_text(text: str, normalization_options: NormalizationOptions) -> str:
"""Normalize text for TTS processing"""

# Handle email addresses first if enabled
if normalization_options.email_normalization:
text = EMAIL_PATTERN.sub(handle_email, text)
Expand Down Expand Up @@ -455,9 +456,9 @@ def normalize_text(text: str, normalization_options: NormalizationOptions) -> st

# Handle special characters that might cause audio artifacts first
# Replace newlines with spaces (or pauses if needed)
text = text.replace('\n', ' ')
text = text.replace('\r', ' ')
text = text.replace("\n", " ")
text = text.replace("\r", " ")

# Handle titles and abbreviations
text = re.sub(r"\bD[Rr]\.(?= [A-Z])", "Doctor", text)
text = re.sub(r"\b(?:Mr\.|MR\.(?= [A-Z]))", "Mister", text)
Expand Down
8 changes: 4 additions & 4 deletions api/src/services/text_processing/phonemizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

import phonemizer

from .normalizer import normalize_text
from ...structures.schemas import NormalizationOptions
from .normalizer import normalize_text

phonemizers = {}

Expand Down Expand Up @@ -95,13 +95,13 @@ def phonemize(text: str, language: str = "a") -> str:
Phonemized text
"""
global phonemizers

# Strip input text first to remove problematic leading/trailing spaces
text = text.strip()

if language not in phonemizers:
phonemizers[language] = create_phonemizer(language)

result = phonemizers[language].phonemize(text)
# Final strip to ensure no leading/trailing spaces in phonemes
return result.strip()
21 changes: 13 additions & 8 deletions api/src/services/text_processing/text_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import re
import time
from typing import AsyncGenerator, Dict, List, Tuple, Optional
from typing import AsyncGenerator, Dict, List, Optional, Tuple

from loguru import logger

Expand Down Expand Up @@ -34,10 +34,10 @@ def process_text_chunk(
List of token IDs
"""
start_time = time.time()

# Strip input text to remove any leading/trailing spaces that could cause artifacts
text = text.strip()

if not text:
return []

Expand Down Expand Up @@ -140,7 +140,7 @@ async def smart_split(
normalization_options: NormalizationOptions = NormalizationOptions(),
) -> AsyncGenerator[Tuple[str, List[int], Optional[float]], None]:
"""Build optimal chunks targeting 300-400 tokens, never exceeding max_tokens.

Yields:
Tuple of (text_chunk, tokens, pause_duration_s).
If pause_duration_s is not None, it's a pause chunk with empty text/tokens.
Expand All @@ -161,7 +161,9 @@ async def smart_split(
part_idx += 1

# --- Process Text Part ---
if text_part_raw and text_part_raw.strip(): # Only process if the part is not empty string
if (
text_part_raw and text_part_raw.strip()
): # Only process if the part is not empty string
# Strip leading and trailing spaces to prevent pause tag splitting artifacts
text_part_raw = text_part_raw.strip()

Expand All @@ -171,8 +173,9 @@ async def smart_split(
if lang_code in ["a", "b", "en-us", "en-gb"]:
processed_text = CUSTOM_PHONEMES.split(processed_text)
for index in range(0, len(processed_text), 2):
processed_text[index] = normalize_text(processed_text[index], normalization_options)

processed_text[index] = normalize_text(
processed_text[index], normalization_options
)

processed_text = "".join(processed_text).strip()
else:
Expand Down Expand Up @@ -316,7 +319,9 @@ async def smart_split(
yield "", [], duration # Yield pause chunk
except (ValueError, TypeError):
# This case should be rare if re.fullmatch passed, but handle anyway
logger.warning(f"Could not parse valid-looking pause duration: {duration_str}")
logger.warning(
f"Could not parse valid-looking pause duration: {duration_str}"
)

# --- End of parts loop ---
total_time = time.time() - start_time
Expand Down
Loading
Loading