Skip to content

Commit fdab06b

Browse files
committed
feat: Enhance video information retrieval and subtitle rendering
- Updated `_get_video_info` function to return video resolution and duration, improving error handling for resolution extraction. - Refactored calls to `_get_video_info` in `render_rounded_video` to utilize the new duration data. - Streamlined code formatting for better readability across various functions in `rounded_renderer.py` and `subtitle_thread.py`. - Improved context handling in `SubtitleThread` for better subtitle optimization based on video file context. These changes enhance the functionality and maintainability of subtitle rendering and video processing features.
1 parent 676c879 commit fdab06b

File tree

2 files changed

+46
-58
lines changed

2 files changed

+46
-58
lines changed

app/core/subtitle/rounded_renderer.py

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,31 @@
2323
logger = setup_logger("subtitle.rounded")
2424

2525

26-
def _get_video_resolution(video_path: str) -> Tuple[int, int]:
27-
"""获取视频分辨率"""
26+
def _get_video_info(video_path: str) -> Tuple[int, int, float]:
27+
"""获取视频分辨率和时长"""
2828
result = subprocess.run(
2929
["ffmpeg", "-i", video_path],
3030
capture_output=True,
3131
text=True,
3232
encoding="utf-8",
3333
errors="replace",
34-
creationflags=(
35-
getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0
36-
),
34+
creationflags=(getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0),
3735
)
3836

37+
# 解析分辨率
38+
width, height = 0, 0
3939
if match := re.search(r"Stream.*Video:.* (\d{2,5})x(\d{2,5})", result.stderr):
40-
return int(match.group(1)), int(match.group(2))
40+
width, height = int(match.group(1)), int(match.group(2))
41+
else:
42+
raise ValueError(f"无法获取视频分辨率: {video_path}")
43+
44+
# 解析时长
45+
duration = 0.0
46+
if match := re.search(r"Duration:\s*(\d+):(\d+):(\d+(?:\.\d+)?)", result.stderr):
47+
h, m, s = match.groups()
48+
duration = int(h) * 3600 + int(m) * 60 + float(s)
4149

42-
raise ValueError(f"无法获取视频分辨率: {video_path}")
50+
return width, height, duration
4351

4452

4553
def render_text_block(
@@ -154,9 +162,7 @@ def render_subtitle_image(
154162
else []
155163
)
156164
secondary_lines = (
157-
wrap_text(
158-
secondary_text, font, width, style.padding_h, extra_margin=extra_margin
159-
)
165+
wrap_text(secondary_text, font, width, style.padding_h, extra_margin=extra_margin)
160166
if secondary_text
161167
else []
162168
)
@@ -169,11 +175,7 @@ def calc_block_height(lines: List[str]) -> float:
169175
return 0
170176
bbox = font.getbbox("测试Ag")
171177
line_h = bbox[3] - bbox[1]
172-
return (
173-
line_h * len(lines)
174-
+ style.line_spacing * (len(lines) - 1)
175-
+ style.padding_v * 2
176-
)
178+
return line_h * len(lines) + style.line_spacing * (len(lines) - 1) + style.padding_v * 2
177179

178180
primary_height = calc_block_height(primary_lines)
179181
secondary_height = calc_block_height(secondary_lines)
@@ -254,15 +256,11 @@ def render_preview(
254256
)
255257

256258
# 渲染字幕并叠加
257-
subtitle_img = render_subtitle_image(
258-
primary_text, secondary_text, width, height, style
259-
)
259+
subtitle_img = render_subtitle_image(primary_text, secondary_text, width, height, style)
260260
background.paste(subtitle_img, (0, 0), subtitle_img)
261261

262262
# 保存到临时目录
263-
with tempfile.NamedTemporaryFile(
264-
mode="wb", suffix=".png", delete=False
265-
) as tmp_file:
263+
with tempfile.NamedTemporaryFile(mode="wb", suffix=".png", delete=False) as tmp_file:
266264
background.save(tmp_file, "PNG")
267265
return tmp_file.name
268266

@@ -302,8 +300,7 @@ def render_rounded_video(
302300
# 检查布局合理性
303301
if layout == SubtitleLayoutEnum.ONLY_TRANSLATE:
304302
has_translation = any(
305-
seg.translated_text and seg.translated_text.strip()
306-
for seg in asr_data.segments
303+
seg.translated_text and seg.translated_text.strip() for seg in asr_data.segments
307304
)
308305
if not has_translation:
309306
layout = SubtitleLayoutEnum.ONLY_ORIGINAL
@@ -312,14 +309,13 @@ def render_rounded_video(
312309
or layout == SubtitleLayoutEnum.ORIGINAL_ON_TOP
313310
):
314311
has_translation = any(
315-
seg.translated_text and seg.translated_text.strip()
316-
for seg in asr_data.segments
312+
seg.translated_text and seg.translated_text.strip() for seg in asr_data.segments
317313
)
318314
if not has_translation:
319315
layout = SubtitleLayoutEnum.ONLY_ORIGINAL
320316

321317
# 获取视频信息
322-
width, height = _get_video_resolution(video_path)
318+
width, height, video_duration = _get_video_info(video_path)
323319

324320
# 构建并缩放样式
325321
style_config = rounded_style or {}
@@ -343,9 +339,7 @@ def render_rounded_video(
343339
temp_path = Path(temp_dir)
344340

345341
# 步骤1: 生成所有字幕PNG (0-30%)
346-
logger.info(
347-
f"生成字幕PNG图片(共{len(asr_data.segments)}个,布局:{layout.value})"
348-
)
342+
logger.info(f"生成字幕PNG图片(共{len(asr_data.segments)}个,布局:{layout.value})")
349343
subtitle_frames = []
350344

351345
for i, seg in enumerate(asr_data.segments):
@@ -372,9 +366,7 @@ def render_rounded_video(
372366
# 进度回调
373367
if progress_callback:
374368
progress = int((i + 1) / len(asr_data.segments) * 30)
375-
progress_callback(
376-
progress, f"生成字幕图片 {i + 1}/{len(asr_data.segments)}"
377-
)
369+
progress_callback(progress, f"生成字幕图片 {i + 1}/{len(asr_data.segments)}")
378370

379371
if not subtitle_frames:
380372
raise ValueError("没有生成任何有效的字幕图片")
@@ -409,14 +401,12 @@ def render_rounded_video(
409401
# 判断是否是最后一批
410402
is_last_batch = batch_idx == total_batches - 1
411403
batch_output = (
412-
output_path
413-
if is_last_batch
414-
else temp_path / f"batch_{batch_idx:03d}.mp4"
404+
output_path if is_last_batch else temp_path / f"batch_{batch_idx:03d}.mp4"
415405
)
416406

417-
logger.info(
418-
f"处理批次 {batch_idx + 1}/{total_batches}{len(batch_frames)}个字幕)"
419-
)
407+
logger.info(f"处理批次 {batch_idx + 1}/{total_batches}{len(batch_frames)}个字幕)")
408+
# 构建 ffmpeg 命令
409+
# -t 参数强制保持原视频时长,防止因 overlay 结束而截断视频
420410
cmd = [
421411
"ffmpeg",
422412
"-y",
@@ -426,7 +416,9 @@ def render_rounded_video(
426416
"-map",
427417
final_output,
428418
"-map",
429-
"0:a?", # 每一批都需要映射音频流
419+
"0:a?",
420+
"-t",
421+
str(video_duration), # 强制保持原视频时长
430422
"-c:v",
431423
"libx264",
432424
"-preset",
@@ -436,7 +428,7 @@ def render_rounded_video(
436428
"-pix_fmt",
437429
"yuv420p",
438430
"-c:a",
439-
"copy", # 每一批都复制音频
431+
"copy",
440432
str(batch_output),
441433
]
442434

@@ -448,6 +440,8 @@ def render_rounded_video(
448440
cmd,
449441
capture_output=True,
450442
text=True,
443+
encoding="utf-8",
444+
errors="replace",
451445
creationflags=(
452446
getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0
453447
),

app/thread/subtitle_thread.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,12 @@ def _setup_llm_config(self) -> Optional[SubtitleConfig]:
7171

7272
def run(self):
7373
# 设置任务上下文
74-
file_name = Path(self.task.subtitle_path).name if self.task.subtitle_path else ""
74+
task_file = (
75+
Path(self.task.video_path) if self.task.video_path else Path(self.task.subtitle_path)
76+
)
7577
set_task_context(
7678
task_id=self.task.task_id,
77-
file_name=file_name,
79+
file_name=task_file.name,
7880
stage="subtitle",
7981
)
8082

@@ -115,7 +117,8 @@ def run(self):
115117
self.update_all.emit(asr_data.to_json())
116118

117119
# 3. 优化字幕
118-
custom_prompt = subtitle_config.custom_prompt_text
120+
context_info = f'The subtitles below are from a file named "{task_file}". Use this context to improve accuracy if needed.\n'
121+
custom_prompt = context_info + (subtitle_config.custom_prompt_text or "") + "\n"
119122
self.subtitle_length = len(asr_data.segments)
120123

121124
if subtitle_config.need_optimize:
@@ -175,9 +178,7 @@ def run(self):
175178
update_callback=self.callback,
176179
)
177180
elif translator_service == TranslatorServiceEnum.DEEPLX:
178-
os.environ["DEEPLX_ENDPOINT"] = (
179-
subtitle_config.deeplx_endpoint or ""
180-
)
181+
os.environ["DEEPLX_ENDPOINT"] = subtitle_config.deeplx_endpoint or ""
181182
translator = DeepLXTranslator(
182183
thread_num=subtitle_config.thread_num,
183184
batch_num=5,
@@ -212,25 +213,22 @@ def run(self):
212213
asr_data.save(
213214
save_path=self.task.output_path or "",
214215
ass_style=subtitle_config.subtitle_style or "",
215-
layout=subtitle_config.subtitle_layout
216-
or SubtitleLayoutEnum.ONLY_TRANSLATE,
216+
layout=subtitle_config.subtitle_layout or SubtitleLayoutEnum.ONLY_TRANSLATE,
217217
)
218218
logger.info(f"字幕保存到 {self.task.output_path}")
219219

220220
# 6. 文件移动与清理
221221
if self.task.need_next_task and self.task.video_path:
222222
# 保存srt/ass文件到视频目录(对于全流程任务)
223223
save_srt_path = (
224-
Path(self.task.video_path).parent
225-
/ f"{Path(self.task.video_path).stem}.srt"
224+
Path(self.task.video_path).parent / f"{Path(self.task.video_path).stem}.srt"
226225
)
227226
asr_data.to_srt(
228227
save_path=str(save_srt_path),
229228
layout=subtitle_config.subtitle_layout,
230229
)
231230
save_ass_path = (
232-
Path(self.task.video_path).parent
233-
/ f"{Path(self.task.video_path).stem}.ass"
231+
Path(self.task.video_path).parent / f"{Path(self.task.video_path).stem}.ass"
234232
)
235233
asr_data.to_ass(
236234
save_path=str(save_ass_path),
@@ -267,15 +265,11 @@ def need_llm(self, subtitle_config: SubtitleConfig, asr_data: ASRData):
267265
def callback(self, result: List[SubtitleProcessData]):
268266
self.finished_subtitle_length += len(result)
269267
# 简单计算当前进度(0-100%)
270-
progress = min(
271-
int((self.finished_subtitle_length / self.subtitle_length) * 100), 100
272-
)
268+
progress = min(int((self.finished_subtitle_length / self.subtitle_length) * 100), 100)
273269
self.progress.emit(progress, self.tr("{0}% 处理字幕").format(progress))
274270
# 转换为字典格式供UI使用
275271
result_dict = {
276-
str(data.index): data.translated_text
277-
or data.optimized_text
278-
or data.original_text
272+
str(data.index): data.translated_text or data.optimized_text or data.original_text
279273
for data in result
280274
}
281275
self.update.emit(result_dict)

0 commit comments

Comments
 (0)