feat(#632): 配音素材库音频架构统一,迁移到audio图层混音 #750
@@ -483,6 +483,7 @@ class RenderAdapter:
|
||||
progress_cb: ProgressCallback | None = None,
|
||||
rendered_clip_ids: list[str] | None = None,
|
||||
failed_clip_ids: list[str] | None = None,
|
||||
voiceover_audio_path: str | None = None,
|
||||
) -> RenderAdapterResult:
|
||||
"""执行统一渲染核心流程(BGM + ASR + 渲染 + 缩略图 + 上传)。
|
||||
|
||||
@@ -491,6 +492,7 @@ class RenderAdapter:
|
||||
Args:
|
||||
rendered_clip_ids: 成功下载/准备的 clip id 列表(render_plan 从下载阶段传入)
|
||||
failed_clip_ids: 失败的 clip id 列表
|
||||
voiceover_audio_path: 配音素材库音频本地路径(一键生成场景使用)
|
||||
|
||||
Returns:
|
||||
RenderAdapterResult
|
||||
@@ -525,6 +527,7 @@ class RenderAdapter:
|
||||
output_height=output_height,
|
||||
bgm_path=bgm_path,
|
||||
asr_service=asr_service,
|
||||
voiceover_audio_path=voiceover_audio_path,
|
||||
)
|
||||
result = render_svc.render()
|
||||
|
||||
@@ -593,6 +596,7 @@ class RenderAdapter:
|
||||
job_id: str = "",
|
||||
work_dir: Path | None = None,
|
||||
progress_cb: ProgressCallback | None = None,
|
||||
voiceover_audio_path: str | None = None,
|
||||
) -> RenderAdapterResult:
|
||||
"""使用内存中的 plan/clips/asset_path_map 直接渲染。
|
||||
|
||||
@@ -606,6 +610,7 @@ class RenderAdapter:
|
||||
job_id: 关联的 Job ID
|
||||
work_dir: 工作目录,不传则用临时目录
|
||||
progress_cb: 进度回调
|
||||
voiceover_audio_path: 配音素材库音频本地路径
|
||||
|
||||
Returns:
|
||||
RenderAdapterResult
|
||||
@@ -649,6 +654,7 @@ class RenderAdapter:
|
||||
plan_id=actual_plan_id,
|
||||
job_id=job_id,
|
||||
progress_cb=progress_cb,
|
||||
voiceover_audio_path=voiceover_audio_path,
|
||||
)
|
||||
|
||||
except subprocess.CalledProcessError as exc:
|
||||
|
||||
@@ -175,6 +175,7 @@ class UnifiedRenderService:
|
||||
transition_duration: float = DEFAULT_TRANSITION_DURATION,
|
||||
asr_service: Any = None, # ASRService 实例,用于自动生成字幕
|
||||
bgm_path: str | None = None, # BGM 本地文件路径
|
||||
voiceover_audio_path: str | None = None, # 配音素材库音频本地路径
|
||||
):
|
||||
self.plan = plan
|
||||
self.clips = clips
|
||||
@@ -186,6 +187,7 @@ class UnifiedRenderService:
|
||||
self.transition_duration = transition_duration
|
||||
self.asr_service = asr_service
|
||||
self.bgm_path = bgm_path
|
||||
self.voiceover_audio_path = voiceover_audio_path
|
||||
self._transition_engine = TransitionEngine(default_duration=transition_duration)
|
||||
self._speed_engine = SpeedEngine()
|
||||
self._asr_timeline_cache: Any = None # ASR 字幕结果缓存,避免重复调用
|
||||
@@ -226,6 +228,9 @@ class UnifiedRenderService:
|
||||
# 3.5 TTS 配音生成(如果配置了)
|
||||
self._maybe_add_voiceover_layer(layers, video_duration=video_duration)
|
||||
|
||||
# 3.6 配音素材库音频(如果传入了本地路径)
|
||||
self._maybe_add_voice_library_layer(layers, video_duration=video_duration)
|
||||
|
||||
# 4. 生成 ASS 字幕文件(如果有 title/subtitle 配置)
|
||||
ass_path = self._maybe_generate_ass(video_duration)
|
||||
|
||||
@@ -843,6 +848,69 @@ class UnifiedRenderService:
|
||||
logger.warning("TTS 配音异常,跳过: %s", e)
|
||||
return False
|
||||
|
||||
def _maybe_add_voice_library_layer(
|
||||
self,
|
||||
layers: list[RenderLayer],
|
||||
*,
|
||||
video_duration: float,
|
||||
) -> bool:
|
||||
"""将配音素材库音频作为整段配音加到 audio 图层.
|
||||
|
||||
与 TTS 配音共享同一套 audio 图层混音架构,
|
||||
支持与 BGM、TTS 的音量平衡,不再走独立的后处理 mux 链路。
|
||||
|
||||
Returns:
|
||||
是否成功添加了配音音轨
|
||||
"""
|
||||
if not self.voiceover_audio_path:
|
||||
return False
|
||||
|
||||
audio_path = Path(self.voiceover_audio_path)
|
||||
if not audio_path.exists() or audio_path.stat().st_size == 0:
|
||||
logger.warning("配音素材库音频文件不存在或为空,跳过: %s", self.voiceover_audio_path)
|
||||
return False
|
||||
|
||||
try:
|
||||
# 找到或创建 audio 图层
|
||||
audio_layer = None
|
||||
for layer in layers:
|
||||
if layer.role == "audio":
|
||||
audio_layer = layer
|
||||
break
|
||||
|
||||
if audio_layer is None:
|
||||
from video_processing.unified_render_service import _LAYER_Z_INDEX # type: ignore
|
||||
|
||||
z_index = _LAYER_Z_INDEX.get("audio", 2)
|
||||
audio_layer = RenderLayer(role="audio", z_index=z_index)
|
||||
layers.append(audio_layer)
|
||||
|
||||
# 配音素材作为整段配音:从 0 开始,覆盖整个视频时长
|
||||
# 音频不足视频时长时,混音层会按实际长度处理(amix 不自动循环)
|
||||
vo_clip = ResolvedClip(
|
||||
clip_id="voice_library_main",
|
||||
asset_id="voice_library",
|
||||
local_path=audio_path,
|
||||
clip_type="audio",
|
||||
order=len(audio_layer.clips),
|
||||
start_time=0.0,
|
||||
duration=video_duration,
|
||||
config={"volume": 1.0, "voice_library": True},
|
||||
actual_duration=video_duration,
|
||||
)
|
||||
audio_layer.clips.append(vo_clip)
|
||||
|
||||
logger.info(
|
||||
"配音素材库音频已添加到 audio 图层: plan_id=%s duration=%.2fs",
|
||||
self.plan.id,
|
||||
video_duration,
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("配音素材库音频添加失败,跳过: %s", e)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _resolve_watermark_config(plan_config: dict[str, Any] | None) -> WatermarkConfig | None:
|
||||
"""从 plan config 中解析水印配置,兼容两种存储格式.
|
||||
|
||||
@@ -1238,6 +1238,7 @@ def _render_video(
|
||||
plan_id=f"gen_{task_id}",
|
||||
job_id=task_id,
|
||||
work_dir=temp_path,
|
||||
voiceover_audio_path=voice_path,
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
@@ -1256,17 +1257,8 @@ def _render_video(
|
||||
render_duration,
|
||||
)
|
||||
|
||||
# 配音混音(素材库音频,后处理混音)
|
||||
if voice_path:
|
||||
final_path = temp_path / f"final-{task_id}.mp4"
|
||||
try:
|
||||
_mux_audio_track(render_output_path, voice_path, final_path)
|
||||
output_path = final_path
|
||||
except Exception as mux_err:
|
||||
logger.warning("[task_id=%s] [混音] 音频混合失败,使用无音频版本: %s", task_id, mux_err)
|
||||
output_path = render_output_path
|
||||
else:
|
||||
output_path = render_output_path
|
||||
# 配音素材库音频已在统一渲染引擎内部通过 audio 图层混音处理
|
||||
output_path = render_output_path
|
||||
|
||||
return output_path, render_duration
|
||||
|
||||
|
||||
Reference in New Issue
Block a user