Merge pull request #3714 from rudycelekli/fix/cow50-voice-identical-text-filenames-20261012

fix(voice): isolate output files for repeated identical speech
This commit is contained in:
zhayujie authored and GitHub committed 2026-10-16 17:12:00 +08:00
commit dfaa53e8af
10 files changed
+58 -15

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
import importlib
import importlib.util
from pathlib import Path
import sys
import time
from types import SimpleNamespace
import pytest
from bridge.reply import ReplyType
@pytest.mark.parametrize('provider', ['google', 'edge', 'pytts'])
def test_identical_replies_have_independent_output_files(tmp_path, monkeypatch, provider):
if provider in ('google', 'pytts'):
filename = 'test_google_voice_errors.py' if provider == 'google' else 'test_pytts_voice_timeout.py'
spec = importlib.util.spec_from_file_location('voice_' + provider + '_support', Path(__file__).with_name(filename))
support = importlib.util.module_from_spec(spec)
spec.loader.exec_module(support)
else:
monkeypatch.setitem(sys.modules, 'edge_tts', SimpleNamespace())
module = importlib.import_module('voice.' + provider + '.' + provider + '_voice')
monkeypatch.setattr(time, 'time', lambda: 1000)
monkeypatch.setattr(module.TmpDir, 'path', lambda self: str(tmp_path) + '/')
if provider == 'google':
monkeypatch.setattr(module, 'gTTS', lambda **kw: SimpleNamespace(save=lambda path: Path(path).write_bytes(b'audio')))
voice = module.GoogleVoice()
elif provider == 'edge':
voice = module.EdgeVoice()
async def generate(text, path):
Path(path).write_bytes(b'audio')
monkeypatch.setattr(voice, 'gen_voice', generate)
else:
voice = module.PyttsVoice()
engine = support._FakeEngine()
engine.save_to_file = lambda text, path: Path(path).write_bytes(b'audio')
voice.engine = engine
first = voice.textToVoice('same reply')
Path(first.content).write_bytes(b'first-clip-sentinel')
second = voice.textToVoice('same reply')
assert first.type == second.type == ReplyType.VOICE
assert first.content != second.content
assert Path(first.content).read_bytes() == b'first-clip-sentinel'
+1 -2
View File
@@ -10,7 +10,6 @@ Description:
import http.client
import json
import time
import requests
import datetime
import hashlib
@@ -51,7 +50,7 @@ def text_to_speech_aliyun(url, text, appkey, token):
response = requests.post(url, headers=headers, data=json.dumps(data), timeout=(5, 60), stream=True)
if response.status_code == 200 and response.headers['Content-Type'] == 'audio/mpeg':
output_file = TmpDir().path() + "reply-" + str(int(time.time())) + "-" + str(hash(text) & 0x7FFFFFFF) + ".wav"
output_file = TmpDir().path() + "reply-" + uuid.uuid4().hex + ".wav"
try:
save_response(response, output_file, MAX_FILE_BYTES)
+2 -2
View File
@@ -3,7 +3,7 @@ azure voice service
"""
import json
import os
import time
import uuid
import azure.cognitiveservices.speech as speechsdk
from langid import classify
@@ -81,7 +81,7 @@ class AzureVoice(Voice):
else:
self.speech_config.speech_synthesis_voice_name = self.config["speech_synthesis_voice_name"]
# Avoid the same filename under multithreading
fileName = TmpDir().path() + "reply-" + str(int(time.time())) + "-" + str(hash(text) & 0x7FFFFFFF) + ".wav"
fileName = TmpDir().path() + "reply-" + uuid.uuid4().hex + ".wav"
audio_config = speechsdk.AudioConfig(filename=fileName)
speech_synthesizer = speechsdk.SpeechSynthesizer(speech_config=self.speech_config, audio_config=audio_config)
result = speech_synthesizer.speak_text(text)
+3 -2
View File
@@ -6,6 +6,7 @@ import os
import time
import threading
import requests
import uuid
from aip import AipSpeech
@@ -157,7 +158,7 @@ class BaiduVoice(Voice):
# used to be buffered whole by `.content` and written out before
# anything could object. `download_to_file` also writes through a temp
# file, so an oversized or truncated download leaves nothing behind.
fn = TmpDir().path() + f"reply-long-{int(time.time())}-{hash(text)&0x7FFFFFFF}.mp3"
fn = TmpDir().path() + f"reply-long-{uuid.uuid4().hex}.mp3"
try:
download_to_file(audio_url, fn, MAX_FILE_BYTES, timeout=REQUEST_TIMEOUT)
except Exception as e:
@@ -177,7 +178,7 @@ class BaiduVoice(Voice):
{"spd":self.spd, "pit":self.pit, "vol":self.vol, "per":self.per}
)
if not isinstance(result, dict):
fn = TmpDir().path() + f"reply-{int(time.time())}-{hash(text)&0x7FFFFFFF}.mp3"
fn = TmpDir().path() + f"reply-{uuid.uuid4().hex}.mp3"
with open(fn, "wb") as f:
f.write(result)
logger.info("[Baidu] 短文本合成 success: %s", fn)
+2 -2
View File
@@ -1,5 +1,5 @@
# Requires: edge-tts (pip install edge-tts)
import time
import uuid
import edge_tts
import asyncio
@@ -43,7 +43,7 @@ class EdgeVoice(Voice):
await communicate.save(fileName)
def textToVoice(self, text):
fileName = TmpDir().path() + "reply-" + str(int(time.time())) + "-" + str(hash(text) & 0x7FFFFFFF) + ".mp3"
fileName = TmpDir().path() + "reply-" + uuid.uuid4().hex + ".mp3"
try:
asyncio.run(self.gen_voice(text, fileName))
+2 -2
View File
@@ -1,5 +1,5 @@
# Requires: elevenlabs==1.0.3 (pip install elevenlabs==1.0.3)
import time
import uuid
from elevenlabs.client import ElevenLabs
from elevenlabs import save
@@ -22,7 +22,7 @@ class ElevenLabsVoice(Voice):
pass
def textToVoice(self, text):
fileName = TmpDir().path() + "reply-" + str(int(time.time())) + "-" + str(hash(text) & 0x7FFFFFFF) + ".mp3"
fileName = TmpDir().path() + "reply-" + uuid.uuid4().hex + ".mp3"
try:
audio = client.generate(
+2 -2
View File
@@ -2,7 +2,7 @@
google voice service
"""
import time
import uuid
import speech_recognition
from gtts import gTTS
@@ -39,7 +39,7 @@ class GoogleVoice(Voice):
def textToVoice(self, text):
try:
# Avoid the same filename under multithreading
mp3File = TmpDir().path() + "reply-" + str(int(time.time())) + "-" + str(hash(text) & 0x7FFFFFFF) + ".mp3"
mp3File = TmpDir().path() + "reply-" + uuid.uuid4().hex + ".mp3"
tts = gTTS(text=text, lang="zh")
tts.save(mp3File)
logger.info("[Google] textToVoice text={} voice file name={}".format(text, mp3File))
+2 -1
View File
@@ -5,6 +5,7 @@ pytts voice service (offline)
import os
import sys
import time
import uuid
import pyttsx3
@@ -41,7 +42,7 @@ class PyttsVoice(Voice):
def textToVoice(self, text):
try:
# Avoid the same filename under multithreading
wavFileName = "reply-" + str(int(time.time())) + "-" + str(hash(text) & 0x7FFFFFFF) + ".wav"
wavFileName = "reply-" + uuid.uuid4().hex + ".wav"
wavFile = TmpDir().path() + wavFileName
logger.info("[Pytts] textToVoice text={} voice file name={}".format(text, wavFile))
+2 -1
View File
@@ -2,6 +2,7 @@ import json
import base64
import os
import time
import uuid
from voice.voice import Voice
from common.log import logger
from tencentcloud.common import credential
@@ -105,7 +106,7 @@ class TencentVoice(Voice):
response = client.TextToVoice(req)
if response.Audio:
fileName = TmpDir().path() + "reply-" + str(int(time.time())) + "-" + str(hash(text) & 0x7FFFFFFF) + ".mp3"
fileName = TmpDir().path() + "reply-" + uuid.uuid4().hex + ".mp3"
with open(fileName, "wb") as f:
f.write(base64.b64decode(response.Audio))
logger.info("[Tencent] textToVoice text={} voice file name={}".format(text, fileName))
+2 -1
View File
@@ -29,6 +29,7 @@ from voice.voice import Voice
from .xunfei_asr import xunfei_asr
from .xunfei_tts import xunfei_tts
import shutil
import uuid
try:
from voice.audio_convert import any_to_mp3
@@ -81,7 +82,7 @@ class XunfeiVoice(Voice):
def textToVoice(self, text):
try:
# Avoid the same filename under multithreading
fileName = TmpDir().path() + "reply-" + str(int(time.time())) + "-" + str(hash(text) & 0x7FFFFFFF) + ".mp3"
fileName = TmpDir().path() + "reply-" + uuid.uuid4().hex + ".mp3"
xunfei_tts(self.APPID,self.APIKey,self.APISecret,self.BusinessArgsTTS,text,fileName)
logger.info("[Xunfei] textToVoice text={} voice file name={}".format(text, fileName))
reply = Reply(ReplyType.VOICE, fileName)