Обрывает короткие фразы
Обрывает короткие фразы. Например, если фраза из 3-4 слов - окончание файла будет оборванным.
Добавил суффикс с несколькими пробелами и точкой (код в соседней ветке) - вроде бы помогает, но как-то очень спонтанно.
Как это можно исцелить? Коллеги, посоветуйте, пожалуйста.
точно немного обрывает иногда то тексту, и что делать
Миша подскажи, слова в конце фразы
Не понял как суффикс пробел точка, куда добавлять, если текст 100 страниц , соседней ветке где дайте ссылку
Скорее всего это баг F5TTS, а не модели.
Добавлять суффиксы пробовал - не помогло. С референсами играл, без толку.
"Текст 100 страниц" не надо делать. Лучше по абзацу, на мой взгляд.
tomasusername
- спасибо очень благодарен за отклик, вот мучаюсь уже неделю, не могу параметры настроить и давал уже и speed=0.9, NFE=48, chunk=250, все равно разрыв есть , может это от склейки кусков зависит, есть где то, кусок кода , как грамотно чтобы потом склеить аккуратно эти куски wave
- а есть код для разбивки на абзацы 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 реагирует на шумы, как на артикуляцию речи. Поэтому они (шумы), нужны и важны. Тогда получается более качественный инференс.
понятно, попробую, я именно audicity резал
добавьте, прошу меня в группу дал заявку https://t.me/speech_recognition_ru
а чем обрезать лучше тогда , tomasusername , как связаться можно в личку написать
добавьте, прошу меня в группу дал заявку https://t.me/speech_recognition_ru
Я в этой группе - только гость.
а чем обрезать лучше тогда , tomasusername , как связаться можно в личку написать
Идея в том, чтобы не обрезать вообще. Оставить "естественное" звучание, и сделать больше паузы. Тогда инференс хороший.
