Обрывает короткие фразы

#38
by tomasusername - opened

Обрывает короткие фразы. Например, если фраза из 3-4 слов - окончание файла будет оборванным.

Добавил суффикс с несколькими пробелами и точкой (код в соседней ветке) - вроде бы помогает, но как-то очень спонтанно.

Как это можно исцелить? Коллеги, посоветуйте, пожалуйста.

точно немного обрывает иногда то тексту, и что делать
Миша подскажи, слова в конце фразы

Не понял как суффикс пробел точка, куда добавлять, если текст 100 страниц , соседней ветке где дайте ссылку

Скорее всего это баг F5TTS, а не модели.

Добавлять суффиксы пробовал - не помогло. С референсами играл, без толку.

"Текст 100 страниц" не надо делать. Лучше по абзацу, на мой взгляд.

tomasusername

  1. спасибо очень благодарен за отклик, вот мучаюсь уже неделю, не могу параметры настроить и давал уже и speed=0.9, NFE=48, chunk=250, все равно разрыв есть , может это от склейки кусков зависит, есть где то, кусок кода , как грамотно чтобы потом склеить аккуратно эти куски wave
  2. а есть код для разбивки на абзацы tomasusername, если можете дайте ссылку прошу хочу сделать аудиокнигу и облом

С обрывами тоже мучаюсь. Я получил более приемлемые результаты, когда сделал хороший референс и уменьшил параметр speed.

Код для разбивки на абцазы? phrases = text.split('\n\n')

tomasusername, я очень благодарен за коммент, да уже понял что просто пустые строки надо добавлять еще нашел
вот это
Замеченная проблема. F5-TTS иногда съедает последнее слово в генерируемом тексте. Чтобы этого избежать с новой строки напишите в конце текста многоточие:
...
https://huggingface.co/Misha24-10/F5-TTS_RUSSIAN/discussions/23

tomasusername а что значит хороший референс, я делаю так, на kaggle noutbook так
vocoder = load_vocoder(vocoder_name="vocos") # Явно для стабильности

'''def load_tts_model(language="ru"):
"""Загрузка TTS модели"""
if language == "ru":
config = RUSSIAN_TTS_MODEL_CFG
print("Loading F5-TTS Russian model...")
else:
config = ENGLISH_TTS_MODEL_CFG
print("Loading F5-TTS English model...")

# HF загрузка
if config[0].startswith("hf://"):
    hf_path = config[0].replace("hf://", "").split("/")
    repo_id = "/".join(hf_path[:2])
    filename = "/".join(hf_path[2:])
    ckpt_path = hf_hub_download(repo_id=repo_id, filename=filename)
else:
    ckpt_path = config[0]

model_cfg = json.loads(config[2])
return load_model(DiT, model_cfg, ckpt_path)

Загружаем русскую по умолчанию

current_model = load_tts_model("ru")
current_language = "ru"
print("Model loaded successfully!")

Функции обработки

def split_text_into_chunks(text: str, max_chars: int = 200) -> List[str]:
sentences = re.split(r'([.!?]+\s+)', text)
chunks = []
current_chunk = ""
for i in range(0, len(sentences), 2):
sentence = sentences[i]
if i + 1 < len(sentences):
sentence += sentences[i + 1]
if len(current_chunk) + len(sentence) <= max_chars:
current_chunk += sentence
else:
if current_chunk:
chunks.append(current_chunk.strip())
current_chunk = sentence
if current_chunk:
chunks.append(current_chunk.strip())
return chunks

def load_text_file(file_path: str) -> str:
encodings = ['utf-8', 'windows-1251', 'cp1252']
for encoding in encodings:
try:
with open(file_path, 'r', encoding=encoding) as f:
return f.read()
except UnicodeDecodeError:
continue
raise ValueError(f"Could not decode file {file_path}")

print("✅ Функции готовы.")'''

def generate_audiobook_manual(
ref_audio_path: str,
ref_text: str,
book_text: str,
output_name: str = "audiobook",
language: str = "ru",
remove_silence: bool = True,
chunk_size: int = 250, # Для абзацев
speed: float = 0.9, # Замедлить для фикса обрезки
seed: int = 10, # Фиксированный
nfe_step: int = 48 # Больше для качества
) -> Tuple[str, str]:
global current_model, current_language, vocoder

print("🚀 Ручной запуск generate_audiobook...")

if not os.path.exists(ref_audio_path):
    raise ValueError(f"Ref аудио не найдено: {ref_audio_path}")
if not book_text.strip():
    raise ValueError("Текст книги пустой!")

if not ref_text or not ref_text.strip():
    print("⚠️ Ref текст не указан! Используем автотранскрипцию (медленно).")

# Seed
if seed != -1:
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    np.random.seed(seed)
    print(f"✓ Seed: {seed}")
else:
    print("⚠️ Случайный seed")

# Модель
if 'current_model' not in globals() or language != current_language:
    print(f"Загрузка модели для {language}...")
    current_model = load_tts_model(language)
    current_language = language

# Ref аудио
print("Подготовка референсного аудио...")
if not ref_text or not ref_text.strip():
    print("⚠️ Транскрибируем с Whisper...")
    ref_audio, ref_text = preprocess_ref_audio_text(ref_audio_path, "", show_info=print)
    print(f"Ref текст: {ref_text[:100]}...")
else:
    print(f"✓ Ref текст: {ref_text}")
    ref_audio, _ = preprocess_ref_audio_text(ref_audio_path, ref_text, show_info=print)

# Чанки по абзацам (фикс обрезки)
print("Разбиение по абзацам...")
chunks = split_by_paragraphs(book_text, max_chars_per_para=chunk_size)
total_chunks = len(chunks)
print(f"✓ {total_chunks} частей, NFE: {nfe_step}")

# Temp dir
temp_dir = tempfile.mkdtemp()
audio_segments = []

# Генерация
for i, chunk in enumerate(chunks):
    print(f"\n--- Часть {i+1}/{total_chunks} ---")
    print(f"Текст: {chunk[:80]}...")
    try:
        final_wave, final_sample_rate, _ = infer_process(
            ref_audio, ref_text, chunk, current_model, vocoder,
            cross_fade_duration=0.2,  # Увеличено для плавного конца
            nfe_step=nfe_step,
            speed=speed,
            sway_sampling_coef=1.0,  # Стабильность
            cfg_strength=2.5,  # Alignment
            show_info=print
        )
        chunk_path = os.path.join(temp_dir, f"chunk_{i+1:04d}.wav")
        sf.write(chunk_path, final_wave, final_sample_rate)
        audio_segments.append((final_wave, final_sample_rate))
        duration = len(final_wave) / final_sample_rate
        print(f"✓ {duration:.1f} сек")
    except Exception as e:
        print(f"❌ Часть {i+1}: {e}")
        import traceback
        traceback.print_exc()
        continue

if not audio_segments:
    raise ValueError("Нет частей!")

# Объединение
print("Объединение...")
combined_audio = np.concatenate([seg[0] for seg in audio_segments])
sample_rate = audio_segments[0][1]
total_duration = len(combined_audio) / sample_rate
print(f"✓ {total_duration/60:.1f} мин")

output_audio_path = os.path.join(temp_dir, f"{output_name}_full.wav")
sf.write(output_audio_path, combined_audio, sample_rate)

# ZIP
print("Архив...")
zip_path = os.path.join(temp_dir, f"{output_name}_parts.zip")
with zipfile.ZipFile(zip_path, 'w') as zipf:
    for file in os.listdir(temp_dir):
        if file.endswith('.wav') and file.startswith('chunk_'):
            zipf.write(os.path.join(temp_dir, file), arcname=file)

print(f"🎉 Готово! Файлы в {temp_dir}")
print(f"   Полный WAV: {output_audio_path}")
print(f"   ZIP с частями: {zip_path}")
print(f"   Seed: {seed if seed != -1 else 'случайный'}, NFE: {nfe_step}")

# Копируем в /kaggle/working/ для скачивания
working_dir = "/kaggle/working/"
os.makedirs(working_dir, exist_ok=True)
final_wav = os.path.join(working_dir, f"{output_name}_full.wav")
final_zip = os.path.join(working_dir, f"{output_name}_parts.zip")
os.system(f"cp {output_audio_path} {final_wav}")
os.system(f"cp {zip_path} {final_zip}")
print(f"✅ Скопировано в /kaggle/working/: {final_wav}, {final_zip}")
print("Скачай через Output панель Kaggle!")

return final_wav, final_zip

print("✅ generate_audiobook_manual готова (с делением по абзацам и фиксами).")

но как узнать на каком месте оборвет слово, получается надо сначала прогонять и определять места где именно сьедает слово
а потом уже обрабатывать текст деля на пустые строки и вставляя, после длинных предложений ставить ...
так получается, ?
tomasusername а у вас есть где то ноутбук как вы добились, можете выложить, поделиться, сравнить

ФУНКЦИЯ РАЗБИВКИ ТЕКСТА

============================================================

def split_text_into_chunks(text: str, max_chars: int = 200):
"""Разбивает текст на части для обработки"""
sentences = re.split(r'([.!?]+\s+)', text)
chunks = []
current_chunk = ""

for i in range(0, len(sentences), 2):
    sentence = sentences[i]
    if i + 1 < len(sentences):
        sentence += sentences[i + 1]
    
    if len(current_chunk) + len(sentence) <= max_chars:
        current_chunk += sentence
    else:
        if current_chunk:
            chunks.append(current_chunk.strip())
        current_chunk = sentence

if current_chunk:
    chunks.append(current_chunk.strip())

return chunks

============================================================

ФУНКЦИЯ ПЛАВНОЙ СКЛЕЙКИ (CROSSFADE)

============================================================

def apply_crossfade(audio1, audio2, fade_samples=2205):
"""
Плавный переход между двумя аудио
fade_samples: количество сэмплов для перехода (~0.05 сек при 44100 Hz)
"""
if len(audio1) < fade_samples or len(audio2) < fade_samples:
# Если части слишком короткие, просто склеиваем
return np.concatenate([audio1, audio2])

# Создаём плавный переход
fade_out = np.linspace(1.0, 0.0, fade_samples)
fade_in = np.linspace(0.0, 1.0, fade_samples)

# Применяем fade к концу первого и началу второго
audio1_end = audio1[-fade_samples:] * fade_out
audio2_start = audio2[:fade_samples] * fade_in

# Смешиваем переходную часть
crossfaded = audio1_end + audio2_start

# Собираем финальное аудио
result = np.concatenate([
    audio1[:-fade_samples],  # Начало первого
    crossfaded,              # Плавный переход
    audio2[fade_samples:]    # Конец второго
])

return result

============================================================

ОСНОВНАЯ ФУНКЦИЯ ГЕНЕРАЦИИ

============================================================

def generate_audiobook_manual(
ref_audio_path: str,
ref_text: str,
book_text: str,
output_name: str = "audiobook",
language: str = "ru",
remove_silence: bool = True,
chunk_size: int = 200,
speed: float = 1.0,
seed: int = -1,
nfe_step: int = 32
):
"""
Генерирует аудиокнигу из текста

Returns:
    tuple: (путь к полному аудио, путь к архиву с частями)
"""
global current_model, current_language, vocoder

print("="*60)
print("🚀 ЗАПУСК ГЕНЕРАЦИИ АУДИОКНИГИ")
print("="*60)

# Проверка входных данных
if not os.path.exists(ref_audio_path):
    raise ValueError(f"❌ Ref аудио не найдено: {ref_audio_path}")
if not book_text.strip():
    raise ValueError("❌ Текст книги пустой!")

print(f"✓ Ref аудио: {ref_audio_path}")
print(f"✓ Ref текст: {ref_text[:50]}...")
print(f"✓ Текст книги: {len(book_text)} символов")

# Установка seed
if seed != -1:
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    np.random.seed(seed)
    print(f"✓ Seed: {seed}")
else:
    print("⚠️ Используется случайный seed")

# Проверка/загрузка модели
if 'current_model' not in globals() or language != current_language:
    print(f"\n🔄 Загрузка модели для языка: {language}...")
    current_model = load_tts_model(language)
    current_language = language
    print("✓ Модель загружена!")

# Подготовка референсного аудио
print("\n📝 Подготовка референсного аудио...")
if not ref_text or not ref_text.strip():
    print("⚠️ Ref текст не указан! Используем автотранскрипцию Whisper (медленно)...")
    ref_audio, ref_text = preprocess_ref_audio_text(ref_audio_path, "", show_info=print)
    print(f"Транскрибированный текст: {ref_text[:100]}...")
else:
    ref_audio, _ = preprocess_ref_audio_text(ref_audio_path, ref_text, show_info=print)

print("✓ Референсное аудио подготовлено")

# Разбиение текста на части
print(f"\n📄 Разбиение текста на части (max {chunk_size} символов)...")
chunks = split_text_into_chunks(book_text, max_chars=chunk_size)
total_chunks = len(chunks)
print(f"✓ Текст разбит на {total_chunks} частей")
print(f"✓ NFE шагов: {nfe_step}")
print(f"✓ Скорость: {speed}x")

# Создание временной директории
temp_dir = tempfile.mkdtemp()
audio_segments = []

# Генерация аудио для каждой части
print("\n" + "="*60)
print("🎙️ ГЕНЕРАЦИЯ АУДИО")
print("="*60)

for i, chunk in enumerate(chunks):
    print(f"\n[{i+1}/{total_chunks}] Обработка части...")
    print(f"Текст: {chunk[:80]}...")
    
    try:
        # Генерация аудио
        final_wave, final_sample_rate, _ = infer_process(
            ref_audio,
            ref_text,
            chunk,
            current_model,
            vocoder,
            cross_fade_duration=0.15,
            nfe_step=nfe_step,
            speed=speed,
            show_info=print
        )
        
        # Сохранение части
        chunk_path = os.path.join(temp_dir, f"chunk_{i+1:04d}.wav")
        sf.write(chunk_path, final_wave, final_sample_rate)
        audio_segments.append((final_wave, final_sample_rate))
        
        duration = len(final_wave) / final_sample_rate
        print(f"✓ Часть {i+1} сгенерирована ({duration:.1f} сек)")
        
    except Exception as e:
        print(f"❌ Ошибка при генерации части {i+1}: {str(e)}")
        import traceback
        traceback.print_exc()
        continue

if not audio_segments:
    raise ValueError("❌ Не удалось сгенерировать ни одной части!")

print(f"\n✓ Успешно сгенерировано частей: {len(audio_segments)}/{total_chunks}")

# Объединение частей с плавными переходами
print("\n" + "="*60)
print("🔗 ОБЪЕДИНЕНИЕ ЧАСТЕЙ С ПЛАВНЫМИ ПЕРЕХОДАМИ")
print("="*60)

sample_rate = audio_segments[0][1]
fade_samples = int(0.05 * sample_rate)  # 50ms crossfade

print(f"Используется crossfade: {fade_samples} сэмплов (~50ms)")

# Начинаем с первой части
combined_audio = audio_segments[0][0]
print(f"✓ Базовая часть: {len(combined_audio)/sample_rate:.1f} сек")

# Добавляем остальные части с crossfade
for i in range(1, len(audio_segments)):
    next_audio = audio_segments[i][0]
    combined_audio = apply_crossfade(combined_audio, next_audio, fade_samples)
    print(f"✓ Склеена часть {i+1}/{len(audio_segments)}")

total_duration = len(combined_audio) / sample_rate
print(f"\n✓ Итоговая длительность: {total_duration/60:.1f} минут ({total_duration:.1f} секунд)")

# Сохранение полного аудио
print("\n💾 Сохранение файлов...")
output_audio_path = os.path.join(temp_dir, f"{output_name}_full.wav")
sf.write(output_audio_path, combined_audio, sample_rate)
print(f"✓ Полное аудио сохранено: {output_name}_full.wav")

# Создание ZIP архива с частями
zip_path = os.path.join(temp_dir, f"{output_name}_parts.zip")
with zipfile.ZipFile(zip_path, 'w') as zipf:
    for file in os.listdir(temp_dir):
        if file.endswith('.wav') and file.startswith('chunk_'):
            zipf.write(os.path.join(temp_dir, file), arcname=file)
print(f"✓ Архив создан: {output_name}_parts.zip ({len(audio_segments)} файлов)")

# Копирование в /kaggle/working/ для скачивания
print("\n📤 Копирование в /kaggle/working/...")
working_dir = "/kaggle/working/"
os.makedirs(working_dir, exist_ok=True)

final_wav = os.path.join(working_dir, f"{output_name}_full.wav")
final_zip = os.path.join(working_dir, f"{output_name}_parts.zip")

shutil.copy2(output_audio_path, final_wav)
shutil.copy2(zip_path, final_zip)

# Итоговая информация
print("\n" + "="*60)
print("🎉 ГЕНЕРАЦИЯ ЗАВЕРШЕНА!")
print("="*60)
print(f"📄 Полное аудио: {final_wav}")
print(f"📦 Архив с частями: {final_zip}")
print(f"⏱️  Длительность: {total_duration/60:.2f} мин")
print(f"🎲 Seed: {seed if seed != -1 else 'случайный'}")
print(f"⚙️  NFE шаги: {nfe_step}")
print(f"🏃 Скорость: {speed}x")
print(f"📊 Частей сгенерировано: {len(audio_segments)}")
print("="*60)
print("\n📥 Скачайте файлы через: Output → Download")

return final_wav, final_zip

============================================================

ЗАПУСК ГЕНЕРАЦИИ

============================================================

try:
print("\n🎬 Начало генерации аудиокниги...\n")

audio_path, zip_path = generate_audiobook_manual(
    ref_audio_path=REF_AUDIO_PATH,
    ref_text=REF_TEXT,
    book_text=BOOK_TEXT,
    output_name=OUTPUT_NAME,
    language=LANGUAGE,
    remove_silence=REMOVE_SILENCE,
    chunk_size=CHUNK_SIZE,
    speed=SPEED,
    seed=SEED,
    nfe_step=NFE_STEP
)

print("\n✅ ВСЁ ГОТОВО!")
print(f"🎧 Аудиокнига создана: {audio_path}")

except Exception as e:
print("\n" + "="*60)
print("❌ КРИТИЧЕСКАЯ ОШИБКА")
print("="*60)
print(f"Ошибка: {str(e)}")
print("\nПодробности:")
import traceback
traceback.print_exc()
print("="*60)

но как узнать на каком месте оборвет слово, получается надо сначала прогонять и определять места где именно сьедает слово
а потом уже обрабатывать текст деля на пустые строки и вставляя, после длинных предложений ставить ...
так получается, ?
tomasusername а у вас есть где то ноутбук как вы добились, можете выложить, поделиться, сравнить

Я не знаю. Проверяю ушами. Сейчас думаю интерфейс с Flask запилить, чтобы исправлять оперативнее по одной фразе.

Ноутбука нет, код на Питоне. Да, я выложил референсы и образцы в группе в телеграме https://t.me/speech_recognition_ru

tomasusername а что значит хороший референс, я делаю так, на kaggle noutbook так

Я обратил внимание, что если резать шумы фрагментами в Audacity - F5TTS лагает намного больше.

Экспериментально я пришёл к тому, что в самом референсе должны быть более длительные паузы. F5TTS реагирует на шумы, как на артикуляцию речи. Поэтому они (шумы), нужны и важны. Тогда получается более качественный инференс.

Победил!

Я добавил в конец + ' \n " ". \n ... \n '

Всё равно лагает, но теперь очень редко и почти незаметно.

addition

понятно, попробую, я именно audicity резал

добавьте, прошу меня в группу дал заявку https://t.me/speech_recognition_ru

а чем обрезать лучше тогда , tomasusername , как связаться можно в личку написать

добавьте, прошу меня в группу дал заявку https://t.me/speech_recognition_ru

Я в этой группе - только гость.

а чем обрезать лучше тогда , tomasusername , как связаться можно в личку написать

Идея в том, чтобы не обрезать вообще. Оставить "естественное" звучание, и сделать больше паузы. Тогда инференс хороший.

Sign up or log in to comment