Skip to content

Commit 18d577f

Browse files
authored
fix(voice): support non-mp3 custom audio duration
Route custom audio file paths through the ffmpeg-backed duration reader so m4a, wav, aac, and other supported containers can be handled consistently with mp3 files.
1 parent c93afb0 commit 18d577f

2 files changed

Lines changed: 28 additions & 10 deletions

File tree

app/services/voice.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1670,32 +1670,32 @@ def _get_audio_duration_from_submaker(sub_maker: SubMaker):
16701670
return 0.0
16711671
return legacy_offsets[-1][1] / 10000000
16721672

1673-
def _get_audio_duration_from_mp3(mp3_file: str) -> float:
1673+
def _get_audio_duration_from_file(audio_file: str) -> float:
16741674
"""
1675-
获取MP3音频时长
1675+
获取音频文件时长(支持 mp3/m4a/wav/aac 等 ffmpeg 可解码的格式)
16761676
"""
1677-
if not os.path.exists(mp3_file):
1678-
logger.error(f"MP3 file does not exist: {mp3_file}")
1677+
if not os.path.exists(audio_file):
1678+
logger.error(f"audio file does not exist: {audio_file}")
16791679
return 0.0
16801680

16811681
try:
1682-
# Use moviepy to get the duration of the MP3 file
1683-
with AudioFileClip(mp3_file) as audio:
1682+
# Use moviepy (ffmpeg) to read the duration of any supported audio format
1683+
with AudioFileClip(audio_file) as audio:
16841684
return audio.duration # Duration in seconds
16851685
except Exception as e:
1686-
logger.error(f"Failed to get audio duration from MP3: {str(e)}")
1686+
logger.error(f"Failed to get audio duration from file: {str(e)}")
16871687
return 0.0
16881688

16891689
def get_audio_duration(target: Union[str, SubMaker]) -> float:
16901690
"""
16911691
获取音频时长
16921692
如果是SubMaker对象,则从SubMaker中获取时长
1693-
如果是MP3文件,则从MP3文件中获取时长
1693+
如果是音频文件路径,则从音频文件中获取时长(支持 mp3/m4a/wav 等格式)
16941694
"""
16951695
if isinstance(target, SubMaker):
16961696
return _get_audio_duration_from_submaker(target)
1697-
elif isinstance(target, str) and target.endswith(".mp3"):
1698-
return _get_audio_duration_from_mp3(target)
1697+
elif isinstance(target, str):
1698+
return _get_audio_duration_from_file(target)
16991699
else:
17001700
logger.error(f"Invalid target type: {type(target)}")
17011701
return 0.0

test/services/test_voice.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,24 @@ def fake_run(command, capture_output, text, check):
100100
self.assertEqual(len(getattr(sub_maker, "offset", [])), 2)
101101
self.assertGreater(vs.get_audio_duration(sub_maker), 0)
102102

103+
def test_get_audio_duration_accepts_non_mp3_files(self):
104+
"""
105+
自定义音频(custom_audio_file)常见为 m4a/wav/aac 等非 mp3 格式。
106+
get_audio_duration 不应因扩展名不是 .mp3 就报 "Invalid target type" 并返回 0,
107+
而应交给 moviepy(ffmpeg) 读取真实时长。
108+
"""
109+
for path in ("custom-audio.m4a", "voice.wav", "clip.aac"):
110+
with patch.object(vs.os.path, "exists", return_value=True), \
111+
patch.object(vs, "AudioFileClip") as mock_afc:
112+
mock_afc.return_value.__enter__.return_value.duration = 28.89
113+
self.assertEqual(vs.get_audio_duration(path), 28.89)
114+
mock_afc.assert_called_once_with(path)
115+
116+
def test_get_audio_duration_missing_file_returns_zero(self):
117+
"""音频文件不存在时安全返回 0,而不是抛异常或读取失败。"""
118+
with patch.object(vs.os.path, "exists", return_value=False):
119+
self.assertEqual(vs.get_audio_duration("does-not-exist.m4a"), 0.0)
120+
103121
def test_no_voice_alias_none_is_supported_temporarily(self):
104122
"""
105123
兼容 PR #981 曾使用过的 none sentinel,避免少量直接调用 API 的用户

0 commit comments

Comments
 (0)