@@ -976,7 +976,7 @@ class TestAudioMixing:
assert UnifiedRenderService . _clip_effective_duration ( clip ) == 0.0
def test_mix_audio_single_main_clip ( self ) :
""" 单主clip时直接提取音频 。"""
""" 只有 main clip(无独立音频轨)→ 源视频音频被丢弃,返回 None 。"""
clips = [ _make_clip ( " c1 " , " main " , order = 0 , duration = 5.0 ) ]
asset_paths = { " asset_c1.mp4 " : Path ( " /tmp/asset_c1.mp4 " ) }
svc = _make_service ( clips , asset_paths )
@@ -990,23 +990,21 @@ class TestAudioMixing:
ctx = _make_ctx ( )
result = mix_audio ( ctx , layers , 5.0 )
assert result is not None
assert result . name == " audio_plan_001.aac "
mock_run . assert_called_once ( )
# 验证命令包含 -vn(无视频)和 aac 编码
cmd = mock_run . call_args [ 0 ] [ 0 ]
assert " -vn " in cmd
assert " aac " in cmd
# 源视频音频被丢弃,没有独立音频轨 → 无音频
assert result is None
mock_run . assert_not_called ( )
def test_mix_audio_multi_main_clips ( self ) :
""" 多主clip时用 concat拼接音频 。 """
""" 多个独立音频轨用 concat 拼接( main 图层音频被丢弃) 。 """
clips = [
_make_clip ( " c1 " , " main " , order = 0 , duration = 3.0 ) ,
_make_clip ( " c2 " , " main " , order = 1 , duration = 2.0 ) ,
_make_clip ( " c1 " , " main " , order = 0 , duration = 3.0 ) , # 源视频音频被丢弃
_make_clip ( " tts1 " , " main " , order = 0 , duration = 3.0 , config = { " role " : " audio " } ) ,
_make_clip ( " tts2 " , " main " , order = 1 , duration = 2.0 , config = { " role " : " audio " } ) ,
]
asset_paths = {
" asset_c1.mp4 " : Path ( " /tmp/asset_c1.mp4 " ) ,
" asset_c2.mp4 " : Path ( " /tmp/asset_c2 .mp4 " ) ,
" asset_tts1.mp4 " : Path ( " /tmp/asset_tts1 .mp4 " ) ,
" asset_tts2.mp4 " : Path ( " /tmp/asset_tts2.mp4 " ) ,
}
svc = _make_service ( clips , asset_paths )
@@ -1022,15 +1020,16 @@ class TestAudioMixing:
assert result is not None
mock_run . assert_called_once ( )
cmd = mock_run . call_args [ 0 ] [ 0 ]
# 验证有 filter_complex 和 concat
assert " -filter_complex " in cmd
cmd_str = " " . join ( cmd )
# 2 个独立音频轨 concat
assert " concat=n=2:v=0:a=1 " in cmd_str
# main 的 c1 不参与音频(源视频杂音被丢弃)
assert " asset_c1.mp4 " not in cmd_str
def test_mix_audio_with_independent_audio_track ( self ) :
""" 有 独立音频轨时用amix混音 。"""
""" 独立音频轨生效;main 图层源视频音频被丢弃 。 """
clips = [
_make_clip ( " c1 " , " main " , order = 0 , duration = 5.0 ) ,
_make_clip ( " c1 " , " main " , order = 0 , duration = 5.0 ) , # 源视频音频被丢弃
_make_clip (
" bgm1 " ,
" main " ,
@@ -1055,11 +1054,52 @@ class TestAudioMixing:
result = mix_audio ( ctx , layers , 5.0 )
assert result is not None
mock_run . assert_called_once ( )
cmd = mock_run . call_args [ 0 ] [ 0 ]
cmd_str = " " . join ( cmd )
# main 的源视频 c1 不参与音频
assert " asset_c1.mp4 " not in cmd_str
# 独立音频轨 bgm1 作为最终音频生效
assert " asset_bgm1.mp4 " in cmd_str
def test_mix_with_independent_audio_amix ( self ) :
""" 直接验证 mix_with_independent_audio: main 音频 + 独立音频轨走 amix 混音。 """
from video_processing . render_audio import mix_with_independent_audio
from video_processing . unified_render_service import ResolvedClip
main = ResolvedClip (
clip_id = " c1 " ,
asset_id = " asset_c1 " ,
clip_type = " main " ,
order = 0 ,
local_path = Path ( " /tmp/asset_c1.mp4 " ) ,
duration = 5.0 ,
actual_duration = 5.0 ,
config = { } ,
)
aud = ResolvedClip (
clip_id = " bgm1 " ,
asset_id = " asset_bgm1 " ,
clip_type = " main " ,
order = 1 ,
local_path = Path ( " /tmp/asset_bgm1.mp4 " ) ,
duration = 5.0 ,
actual_duration = 5.0 ,
config = { " volume " : 0.5 } ,
)
ctx = _make_ctx ( )
output_path = ctx . work_dir / " audio_plan_001.aac "
with patch ( " video_processing.render_audio.run_ffmpeg " ) as mock_run :
mix_with_independent_audio ( ctx , [ main ] , [ aud ] , output_path , 5.0 )
mock_run . assert_called_once ( )
cmd = mock_run . call_args [ 0 ] [ 0 ]
cmd_str = " " . join ( cmd )
assert " amix " in cmd_str
assert " volume=0.5 " in cmd_str
assert " asset_c1.mp4 " in cmd_str
assert " asset_bgm1.mp4 " in cmd_str
def test_mix_audio_no_audio_returns_none ( self ) :
""" 没有音频素材时返回None。 """
@@ -1082,14 +1122,16 @@ class TestAudioMixing:
assert result is None
def test_mix_audio_background_not_used_as_main ( self ) :
""" background 图层不参与主音频,main 优先级更高 。 """
""" main/ background 图层音频都被丢弃,只有独立音频轨参与混音 。"""
clips = [
_make_clip ( " bg1 " , " background " , order = 0 , duration = 5.0 ) ,
_make_clip ( " c1 " , " main " , order = 0 , duration = 5.0 ) ,
_make_clip ( " tts1 " , " main " , order = 0 , duration = 5.0 , config = { " role " : " audio " } ) ,
]
asset_paths = {
" asset_bg1.mp4 " : Path ( " /tmp/asset_bg1.mp4 " ) ,
" asset_c1.mp4 " : Path ( " /tmp/asset_c1.mp4 " ) ,
" asset_tts1.mp4 " : Path ( " /tmp/asset_tts1.mp4 " ) ,
}
svc = _make_service ( clips , asset_paths )
@@ -1105,21 +1147,24 @@ class TestAudioMixing:
assert result is not None
mock_run . assert_called_once ( )
cmd = mock_run . call_args [ 0 ] [ 0 ]
# 验证主音频源是 main 的 c1,不是 background 的 bg1
# 单 main clip 走直接提取路径,输入文件应该只有 c1
cmd_str = " " . join ( cmd )
assert " asset_c1.mp4 " in cmd_str
# 源视频(background + main)音频都被丢弃
assert " asset_bg1.mp4 " not in cmd_str
assert " asset_c1.mp4 " not in cmd_str
# 只有独立音频轨
assert " asset_tts1.mp4 " in cmd_str
def test_mix_audio_main_priority_over_broll ( self ) :
""" main 图层优先级高于 broll 。 """
""" main/broll 图层的源视频音频都被丢弃,只使用独立音频轨 。 """
clips = [
_make_clip ( " b1 " , " b_roll " , order = 0 , duration = 5.0 ) ,
_make_clip ( " c1 " , " main " , order = 0 , duration = 5.0 ) ,
_make_clip ( " tts1 " , " main " , order = 0 , duration = 5.0 , config = { " role " : " audio " } ) ,
]
asset_paths = {
" asset_b1.mp4 " : Path ( " /tmp/asset_b1.mp4 " ) ,
" asset_c1.mp4 " : Path ( " /tmp/asset_c1.mp4 " ) ,
" asset_tts1.mp4 " : Path ( " /tmp/asset_tts1.mp4 " ) ,
}
svc = _make_service ( clips , asset_paths )
@@ -1136,12 +1181,13 @@ class TestAudioMixing:
mock_run . assert_called_once ( )
cmd = mock_run . call_args [ 0 ] [ 0 ]
cmd_str = " " . join ( cmd )
# 主音频源应该是 main 的 c1,不是 broll 的 b1
assert " asset_c1.mp4 " in cmd_str
# main 和 broll 的源视频音频都被丢弃
assert " asset_c1.mp4 " not in cmd_str
assert " asset_b1.mp4 " not in cmd_str
assert " asset_tts1.mp4 " in cmd_str
def test_mix_audio_broll_used_when_no_main ( self ) :
""" 没有 main 时,broll 作为主音频源 。"""
""" broll 图层源视频音频也被丢弃;无独立音频轨 → 返回 None 。"""
clips = [ _make_clip ( " b1 " , " b_roll " , order = 0 , duration = 5.0 ) ]
asset_paths = { " asset_b1.mp4 " : Path ( " /tmp/asset_b1.mp4 " ) }
svc = _make_service ( clips , asset_paths )
@@ -1155,16 +1201,20 @@ class TestAudioMixing:
ctx = _make_ctx ( )
result = mix_audio ( ctx , layers , 5.0 )
assert result is not None
mock_run . assert_called_once ( )
cmd = mock_run . call_args [ 0 ] [ 0 ]
assert " -vn " in cmd
assert " asset_b1.mp4 " in " " . join ( cmd )
# 源视频音频被丢弃,无独立音频轨 → 无音频
assert result is None
mock_run . assert_not_called ( )
def test_mix_audio_single_clip_truncated_to_video_duration ( self ) :
""" 单clip 音频截断到 video_duration( video_duration < clip有效时长)。 """
clips = [ _make_clip ( " c1 " , " main " , order = 0 , duration = 10.0 ) ]
asset_paths = { " asset_c1.mp4 " : Path ( " /tmp/asset_c1.mp4 " ) }
""" 单独立 音频轨 截断到 video_duration( video_duration < clip有效时长)。 """
clips = [
_make_clip ( " c1 " , " main " , order = 0 , duration = 10.0 ) , # 源视频音频被丢弃
_make_clip ( " tts1 " , " main " , order = 0 , duration = 10.0 , config = { " role " : " audio " } ) ,
]
asset_paths = {
" asset_c1.mp4 " : Path ( " /tmp/asset_c1.mp4 " ) ,
" asset_tts1.mp4 " : Path ( " /tmp/asset_tts1.mp4 " ) ,
}
svc = _make_service ( clips , asset_paths )
with (
@@ -1174,17 +1224,17 @@ class TestAudioMixing:
) :
layers = svc . _group_clips_into_layers ( svc . _resolve_clips ( ) )
ctx = _make_ctx ( )
# video_duration 只有 3.0,小于 clip 的 10.0
# video_duration 只有 3.0,小于独立音频轨 的 10.0
result = mix_audio ( ctx , layers , 3.0 )
assert result is not None
mock_run . assert_called_once ( )
cmd = mock_run . call_args [ 0 ] [ 0 ]
# 验证 -t 参数是 3.0 不是 10.0
t_index = cmd . index ( " -t " )
assert t_index > = 0
t_value = float ( cmd [ t_index + 1 ] )
assert t_value == 3.0
# 验证截断到 3.0( -t 3.0 或 atrim=0:3.000)
cmd_str = " " . join ( cmd )
assert " 3.000 " in cmd_str or " 3.0 " in cmd_str
# 源视频不参与
assert " asset_c1.mp4 " not in cmd_str
def test_merge_audio_video ( self ) :
""" 合并音视频命令正确。 """
@@ -1341,25 +1391,22 @@ class TestAudioMixing:
mock_run . assert_not_called ( )
def test_mix_audio_partial_clips_no_audio_filtered ( self ) :
""" 部分主图层clip无音频流时,过滤掉无音轨的,剩余有音频的正常concat 。"""
""" main 图层音频全部丢弃;独立音频轨有/无音频时按预期过滤 。"""
clips = [
_make_clip ( " c1 " , " main " , order = 0 , duration = 3.0 ) , # 无音频
_make_clip ( " c2 " , " main " , order = 1 , duration = 2.0 ) , # 有音频
_make_clip ( " c1 " , " main " , order = 0 , duration = 3.0 ) , # 源视频音频被丢弃
_make_clip ( " c2 " , " main " , order = 1 , duration = 2.0 ) , # 源视频音频被丢弃
_make_clip ( " tts1 " , " main " , order = 0 , duration = 2.0 , config = { " role " : " audio " } ) , # 独立音频轨
]
asset_paths = {
" asset_c1.mp4 " : Path ( " /tmp/asset_c1.mp4 " ) ,
" asset_c2.mp4 " : Path ( " /tmp/asset_c2.mp4 " ) ,
" asset_tts1.mp4 " : Path ( " /tmp/asset_tts1.mp4 " ) ,
}
svc = _make_service ( clips , asset_paths )
# 模拟:c1 无音频,c2 有音频
def fake_has_audio ( path ) :
return " c2 " in str ( path )
with (
_patch_path_exists ( ) ,
patch ( " video_processing.unified_render_service.probe_duration " , return_value = 5.0 ) ,
patch ( " video_processing.render_audio.probe_has_audio " , side_effect = fake_has_audio ) ,
patch ( " video_processing.render_audio.run_ffmpeg " ) as mock_run ,
) :
layers = svc . _group_clips_into_layers ( svc . _resolve_clips ( ) )
@@ -1370,21 +1417,23 @@ class TestAudioMixing:
mock_run . assert_called_once ( )
cmd = mock_run . call_args [ 0 ] [ 0 ]
cmd_str = " " . join ( cmd )
# 只剩 1 个有效音频 clip,走单clip路径(-vn),不走 filter_complex concat
assert " -vn " in cmd
assert " concat=n=2 " not in cmd_str
# 只有独立音频轨 tts1 参与
assert " asset_tts1.mp4 " in cmd_str
# main 的 c1/c2 源视频音频被丢弃
assert " asset_c1.mp4 " not in cmd_str
assert " asset_c2.mp4 " not in cmd_str
def test_mix_audio_all_main_no_audio_but_independent_track ( self ) :
""" 主图层全部无音频,但有独立音频轨时,正常走amix混音 。"""
""" main 图层源视频音频全部丢弃;仅独立音频轨生效,走 concat 单轨路径 。"""
clips = [
_make_clip ( " c1 " , " main " , order = 0 , duration = 5.0 ) , # 无音频
_make_clip ( " c1 " , " main " , order = 0 , duration = 5.0 ) , # 源视频音频被丢弃
_make_clip (
" bgm1 " ,
" main " ,
order = 0 ,
duration = 5.0 ,
config = { " role " : " audio " , " volume " : 0.5 } ,
) , # 独立音频轨(有音频)
) , # 独立音频轨
]
asset_paths = {
" asset_c1.mp4 " : Path ( " /tmp/asset_c1.mp4 " ) ,
@@ -1392,13 +1441,9 @@ class TestAudioMixing:
}
svc = _make_service ( clips , asset_paths )
def fake_has_audio ( path ) :
return " bgm " in str ( path )
with (
_patch_path_exists ( ) ,
patch ( " video_processing.unified_render_service.probe_duration " , return_value = 5.0 ) ,
patch ( " video_processing.render_audio.probe_has_audio " , side_effect = fake_has_audio ) ,
patch ( " video_processing.render_audio.run_ffmpeg " ) as mock_run ,
) :
layers = svc . _group_clips_into_layers ( svc . _resolve_clips ( ) )
@@ -1409,8 +1454,10 @@ class TestAudioMixing:
mock_run . assert_called_once ( )
cmd = mock_run . call_args [ 0 ] [ 0 ]
cmd_str = " " . join ( cmd )
# 只有独立音频轨参与混音,amix 输入数=1
assert " amix=inputs=1 " in cmd_str
# main 的源视频 c1 不参与音频
assert " asset_c1.mp4 " not in cmd_str
# 只有独立音频轨 bgm1 作为主音频走单轨拼接
assert " asset_bgm1.mp4 " in cmd_str
def test_mix_audio_both_no_audio_returns_none ( self ) :
""" 主图层和独立音频轨都无音频时,返回None。 """
@@ -2069,14 +2116,16 @@ class TestConcatNormalizeAudioFormat:
"""
def test_multi_clip_concat_has_aformat ( self ) :
""" 多 clip 音频 concat 前,每个 clip 都有 aformat 归一化。 """
""" 多独立 音频轨 concat 前,每个轨 都有 aformat 归一化( main 图层源视频音频被丢弃) 。 """
clips = [
_make_clip ( " c1 " , " main " , order = 0 , duration = 3.0 ) ,
_make_clip ( " c2 " , " main " , order = 1 , duration = 2.0 ) ,
_make_clip ( " c1 " , " main " , order = 0 , duration = 3.0 ) , # 源视频音频被丢弃
_make_clip ( " tts1 " , " main " , order = 0 , duration = 3.0 , config = { " role " : " audio " } ) ,
_make_clip ( " tts2 " , " main " , order = 1 , duration = 2.0 , config = { " role " : " audio " } ) ,
]
asset_paths = {
" asset_c1.mp4 " : Path ( " /tmp/asset_c1.mp4 " ) ,
" asset_c2.mp4 " : Path ( " /tmp/asset_c2 .mp4 " ) ,
" asset_tts1.mp4 " : Path ( " /tmp/asset_tts1 .mp4 " ) ,
" asset_tts2.mp4 " : Path ( " /tmp/asset_tts2.mp4 " ) ,
}
svc = _make_service ( clips , asset_paths )
@@ -2099,22 +2148,24 @@ class TestConcatNormalizeAudioFormat:
assert " channel_layouts=stereo " in cmd_str , " 声道应统一为 stereo "
assert " sample_fmts=fltp " in cmd_str , " 采样格式应统一为 fltp "
# 两个 clip 都应该 有 aformat
# 2 个独立音频轨 都应有 aformat
aformat_count = cmd_str . count ( " aformat= " )
assert aformat_count > = 2 , f " 每个 clip 都应有 aformat,实际 { aformat_count } 个 "
assert aformat_count > = 2 , f " 每个独立音频轨 都应有 aformat,实际 { aformat_count } 个 "
# 有 concat
assert " concat=n=2:v=0:a=1 " in cmd_str
# main 源视频不参与
assert " asset_c1.mp4 " not in cmd_str
def test_aformat_before_concat ( self ) :
""" aformat 应在 concat 之前(每个 clip 处理链中 aformat 在 concat 之前)。 """
""" aformat 应在 concat 之前(每个独立音频轨 处理链中 aformat 在 concat 之前)。 """
clips = [
_make_clip ( " c1 " , " main " , order = 0 , duration = 3.0 ) ,
_make_clip ( " c2 " , " main " , order = 1 , duration = 2.0 ) ,
_make_clip ( " tts1 " , " main " , order = 0 , duration = 3.0 , config = { " role " : " audio " } ) ,
_make_clip ( " tts2 " , " main " , order = 1 , duration = 2.0 , config = { " role " : " audio " } ) ,
]
asset_paths = {
" asset_c1.mp4 " : Path ( " /tmp/asset_c 1.mp4 " ) ,
" asset_c2.mp4 " : Path ( " /tmp/asset_c 2.mp4 " ) ,
" asset_tts1.mp4 " : Path ( " /tmp/asset_tts 1.mp4 " ) ,
" asset_tts2.mp4 " : Path ( " /tmp/asset_tts 2.mp4 " ) ,
}
svc = _make_service ( clips , asset_paths )
@@ -2131,19 +2182,23 @@ class TestConcatNormalizeAudioFormat:
fc_idx = cmd . index ( " -filter_complex " )
fc_str = cmd [ fc_idx + 1 ]
# 找到 concat 的位置
concat_pos = fc_str . find ( " concat= " )
assert concat_pos > 0 , " 应找到 concat filter "
# 在 concat 之前的部分应该有 aformat
before_concat = fc_str [ : concat_pos ]
aformat_before_count = before_concat . count ( " aformat= " )
assert aformat_before_count > = 2 , f " concat 之前每个 clip 都应有 aformat,实际 { aformat_before_count } 个 "
assert aformat_before_count > = 2 , f " concat 之前每个独立音频轨 都应有 aformat,实际 { aformat_before_count } 个 "
def test_single_clip_audio_has_normalized_output ( self ) :
""" 单 clip 音频输出也应统一格式(一致性保障)。 """
clips = [ _make_clip ( " c1 " , " main " , order = 0 , duration = 5.0 ) ]
asset_paths = { " asset_c1.mp4 " : Path ( " /tmp/asset_c1.mp4 " ) }
""" 单独立 音频轨 输出也应统一格式(一致性保障)。 """
clips = [
_make_clip ( " c1 " , " main " , order = 0 , duration = 5.0 ) , # 源视频音频被丢弃
_make_clip ( " tts1 " , " main " , order = 0 , duration = 5.0 , config = { " role " : " audio " } ) ,
]
asset_paths = {
" asset_c1.mp4 " : Path ( " /tmp/asset_c1.mp4 " ) ,
" asset_tts1.mp4 " : Path ( " /tmp/asset_tts1.mp4 " ) ,
}
svc = _make_service ( clips , asset_paths )
with (
@@ -2158,18 +2213,20 @@ class TestConcatNormalizeAudioFormat:
assert mock_run . called
cmd = mock_run . call_args [ 0 ] [ 0 ]
# 单 clip 简单路径应该 有 -ar 48000 和 -ac 2
assert " -ar " in cmd , " 单 clip 音频应指定采样率 "
# 单独立音频轨 简单路径应有 -ar 48000 和 -ac 2
assert " -ar " in cmd , " 单独立 音频轨 应指定采样率 "
ar_idx = cmd . index ( " -ar " )
assert cmd [ ar_idx + 1 ] == " 48000 " , " 采样率应为 48000 "
assert " -ac " in cmd , " 单 clip 音频应指定声道数 "
assert " -ac " in cmd , " 单独立 音频轨 应指定声道数 "
ac_idx = cmd . index ( " -ac " )
assert cmd [ ac_idx + 1 ] == " 2 " , " 声道数应为 2( stereo) "
cmd_str = " " . join ( cmd )
assert " asset_c1.mp4 " not in cmd_str
def test_independent_audio_track_has_aformat ( self ) :
""" 独立音频轨在 amix 前也有 aformat 归一化 。 """
""" 独立音频轨输出也应统一格式(48000Hz + stereo + aac) 。 """
clips = [
_make_clip ( " c1 " , " main " , order = 0 , duration = 5.0 ) ,
_make_clip ( " c1 " , " main " , order = 0 , duration = 5.0 ) , # 源视频音频被丢弃
_make_clip (
" audio1 " ,
" main " ,
@@ -2197,12 +2254,16 @@ class TestConcatNormalizeAudioFormat:
cmd = mock_run . call_args [ 0 ] [ 0 ]
cmd_str = " " . join ( cmd )
# 应该有 aformat
assert " aformat= " in cmd_str
assert " sample_rates=48000 " in cmd_str
assert " channel_layouts=stereo " in cmd_str
# amix 应该存在
assert " amix " in cmd_str
# 单独立音频轨走 concat 单轨简单路径:-ar 48000 -ac 2
assert " -ar " in cmd
ar_idx = cmd . index ( " -ar " )
assert cmd [ ar_idx + 1 ] == " 48000 "
assert " -ac " in cmd
ac_idx = cmd . index ( " -ac " )
assert cmd [ ac_idx + 1 ] == " 2 "
# main 源视频 c1 不参与音频
assert " asset_c1.mp4 " not in cmd_str
assert " asset_audio1.mp4 " in cmd_str
class TestConcatNormalizeAudioCodec :
@@ -2212,14 +2273,14 @@ class TestConcatNormalizeAudioCodec:
"""
def test_multi_clip_output_is_aac ( self ) :
""" 多 clip concat 后输出编码为 aac。 """
""" 多独立音频轨 concat 后输出编码为 aac。 """
clips = [
_make_clip ( " c1 " , " main " , order = 0 , duration = 3.0 ) ,
_make_clip ( " c2 " , " main " , order = 1 , duration = 2.0 ) ,
_make_clip ( " tts1 " , " main " , order = 0 , duration = 3.0 , config = { " role " : " audio " } ) ,
_make_clip ( " tts2 " , " main " , order = 1 , duration = 2.0 , config = { " role " : " audio " } ) ,
]
asset_paths = {
" asset_c1.mp4 " : Path ( " /tmp/asset_c 1.mp4 " ) ,
" asset_c2.mp4 " : Path ( " /tmp/asset_c 2.mp4 " ) ,
" asset_tts1.mp4 " : Path ( " /tmp/asset_tts 1.mp4 " ) ,
" asset_tts2.mp4 " : Path ( " /tmp/asset_tts 2.mp4 " ) ,
}
svc = _make_service ( clips , asset_paths )
@@ -2238,9 +2299,15 @@ class TestConcatNormalizeAudioCodec:
assert " -b:a " in cmd , " 应指定音频码率 "
def test_single_clip_output_is_aac ( self ) :
""" 单 clip 音频输出编码为 aac。 """
clips = [ _make_clip ( " c1 " , " main " , order = 0 , duration = 5.0 ) ]
asset_paths = { " asset_c1.mp4 " : Path ( " /tmp/asset_c1.mp4 " ) }
""" 单独立 音频轨 输出编码为 aac。 """
clips = [
_make_clip ( " c1 " , " main " , order = 0 , duration = 5.0 ) , # 源视频音频被丢弃
_make_clip ( " tts1 " , " main " , order = 0 , duration = 5.0 , config = { " role " : " audio " } ) ,
]
asset_paths = {
" asset_c1.mp4 " : Path ( " /tmp/asset_c1.mp4 " ) ,
" asset_tts1.mp4 " : Path ( " /tmp/asset_tts1.mp4 " ) ,
}
svc = _make_service ( clips , asset_paths )
with (
@@ -2255,6 +2322,7 @@ class TestConcatNormalizeAudioCodec:
assert mock_run . called
cmd = mock_run . call_args [ 0 ] [ 0 ]
assert " aac " in cmd
assert " asset_c1.mp4 " not in " " . join ( cmd )
class TestConcatNormalizeFourItemsComplete :
@@ -2304,9 +2372,14 @@ class TestConcatNormalizeFourItemsComplete:
assert fps_count > = 3 , f " 3个 clip 都应有 fps=30,实际 { fps_count } 个 "
def test_audio_format_normalized ( self ) :
""" [3/4] 音频格式:所有 clip concat 前都有 aformat 归一化。 """
clips = self . _make_one_take_clips ( )
""" [3/4] 音频格式:3 个独立音频轨 concat 前都有 aformat 归一化( main 图层源视频音频被丢弃) 。 """
clips = self . _make_one_take_clips ( ) + [
_make_clip ( " tts1 " , " main " , order = 10 , duration = 5.0 , config = { " role " : " audio " } ) ,
_make_clip ( " tts2 " , " main " , order = 11 , duration = 4.0 , config = { " role " : " audio " } ) ,
_make_clip ( " tts3 " , " main " , order = 12 , duration = 3.0 , config = { " role " : " audio " } ) ,
]
asset_paths = { f " asset_c { i } .mp4 " : Path ( f " /tmp/asset_c { i } .mp4 " ) for i in range ( 1 , 4 ) }
asset_paths . update ( { f " asset_tts { i } .mp4 " : Path ( f " /tmp/asset_tts { i } .mp4 " ) for i in range ( 1 , 4 ) } )
svc = _make_service ( clips , asset_paths )
with (
@@ -2323,15 +2396,23 @@ class TestConcatNormalizeFourItemsComplete:
cmd_str = " " . join ( cmd )
aformat_count = cmd_str . count ( " aformat= " )
assert aformat_count > = 3 , f " 3个 clip 都应有 aformat,实际 { aformat_count } 个 "
assert aformat_count > = 3 , f " 3个独立音频轨 都应有 aformat,实际 { aformat_count } 个 "
assert " sample_rates=48000 " in cmd_str
assert " channel_layouts=stereo " in cmd_str
assert " sample_fmts=fltp " in cmd_str
# main 图层源视频不参与
for i in range ( 1 , 4 ) :
assert f " asset_c { i } .mp4 " not in cmd_str
def test_audio_codec_aac ( self ) :
""" [4/4] 音频编码:输出为 aac。 """
clips = self . _make_one_take_clips ( )
""" [4/4] 音频编码:输出为 aac(仅独立音频轨参与) 。 """
clips = self . _make_one_take_clips ( ) + [
_make_clip ( " tts1 " , " main " , order = 10 , duration = 5.0 , config = { " role " : " audio " } ) ,
_make_clip ( " tts2 " , " main " , order = 11 , duration = 4.0 , config = { " role " : " audio " } ) ,
_make_clip ( " tts3 " , " main " , order = 12 , duration = 3.0 , config = { " role " : " audio " } ) ,
]
asset_paths = { f " asset_c { i } .mp4 " : Path ( f " /tmp/asset_c { i } .mp4 " ) for i in range ( 1 , 4 ) }
asset_paths . update ( { f " asset_tts { i } .mp4 " : Path ( f " /tmp/asset_tts { i } .mp4 " ) for i in range ( 1 , 4 ) } )
svc = _make_service ( clips , asset_paths )
with (