videoconverter-worker 1.0.2__tar.gz → 1.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: videoconverter-worker
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: VideoConverter Python Worker:从 queue 目录读取任务并执行切分/去字幕/合成
5
5
  License: MIT
6
6
  Keywords: videoconverter,ffmpeg,worker,video
@@ -285,13 +285,15 @@ def split_video_to_chunks(
285
285
  })
286
286
  logger.info("切分完成: %s (%.1f - %.1f秒)", chunk_id, ch_start, ch_end)
287
287
 
288
+ _now = __import__("datetime").datetime.utcnow()
288
289
  metadata = {
289
290
  "videoId": video_id,
290
291
  "originalPath": video_path,
291
292
  "chunkSize": chunk_size_sec,
292
293
  "totalChunks": total_chunks,
293
294
  "chunks": chunks,
294
- "createdAt": __import__("datetime").datetime.utcnow().isoformat() + "Z",
295
+ "createdAt": _now.isoformat() + "Z",
296
+ "splitStartedAt": _now.isoformat() + "Z",
295
297
  }
296
298
  meta_path = chunk_dir / "metadata.json"
297
299
  with open(meta_path, "w", encoding="utf-8") as f:
@@ -301,20 +303,33 @@ def split_video_to_chunks(
301
303
 
302
304
 
303
305
  def merge_chunks(metadata: dict, start_time: float, end_time: float, output_path: str) -> bool:
304
- """合并已处理的 chunk(按 startTime 排序,concat + 可选 trim)。"""
306
+ """合并已处理的 chunk(按 startTime 排序,concat + 可选 trim)。processedPath 支持相对路径(相对 output_dir/video_id)或绝对路径。"""
305
307
  chunks = metadata.get("chunks") or []
306
308
  processed = [c for c in chunks if c.get("status") == "processed" and c.get("processedPath")]
307
- processed = [c for c in processed if Path(c["processedPath"]).exists()]
309
+ out_path = Path(output_path)
310
+ video_id = metadata.get("videoId") or ""
311
+ chunk_dir = out_path.parent / video_id if video_id else out_path.parent
312
+
313
+ def resolve_path(c: dict) -> Path:
314
+ raw = c["processedPath"]
315
+ p = Path(raw)
316
+ if p.is_absolute():
317
+ return p.resolve()
318
+ return (chunk_dir / raw).resolve()
319
+
320
+ processed = [c for c in processed if resolve_path(c).exists()]
308
321
  if not processed:
309
322
  raise ValueError("没有可用的已处理小块")
310
323
  processed.sort(key=lambda c: c["startTime"])
311
324
 
312
325
  with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
313
326
  for c in processed:
314
- f.write(f"file '{Path(c['processedPath']).resolve()}'\n")
327
+ p = resolve_path(c)
328
+ # FFmpeg concat 列表:路径中单引号须转义为 '\''
329
+ path_str = str(p).replace("'", "'\\''")
330
+ f.write(f"file '{path_str}'\n")
315
331
  list_path = f.name
316
332
  try:
317
- out_path = Path(output_path)
318
333
  tmp_concat = out_path.parent / f"chunk_merge_{os.getpid()}.mp4"
319
334
  tmp_trim = out_path.parent / f"chunk_trim_{os.getpid()}.mp4"
320
335
  try:
@@ -54,13 +54,15 @@ def update_chunk_processed(metadata_path: str, chunk_id: str, processed_path: st
54
54
 
55
55
  def _do_update():
56
56
  data = load_metadata(metadata_path)
57
+ # 存相对路径(相对 metadata 所在目录),便于移动 output 目录后仍可合并
58
+ rel_path = processed_path_obj.name
57
59
  for chunk in data.get("chunks") or []:
58
60
  if chunk.get("chunkId") == chunk_id:
59
- chunk["processedPath"] = processed_path
61
+ chunk["processedPath"] = rel_path
60
62
  chunk["status"] = "processed"
61
63
  chunk["processedAt"] = __import__("datetime").datetime.utcnow().isoformat() + "Z"
62
64
  save_metadata(metadata_path, data)
63
- logger.info("已更新 metadata 中 chunk %s 为已处理: %s", chunk_id, processed_path)
65
+ logger.info("已更新 metadata 中 chunk %s 为已处理: %s", chunk_id, rel_path)
64
66
  return
65
67
  logger.warning("未在 metadata 中找到 chunk: %s", chunk_id)
66
68
 
@@ -71,5 +73,9 @@ def update_chunk_processed(metadata_path: str, chunk_id: str, processed_path: st
71
73
  _do_update()
72
74
  finally:
73
75
  fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
76
+ try:
77
+ lock_path.unlink(missing_ok=True)
78
+ except OSError:
79
+ pass
74
80
  else:
75
81
  _do_update()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "videoconverter-worker"
7
- version = "1.0.2"
7
+ version = "1.0.3"
8
8
  description = "VideoConverter Python Worker:从 queue 目录读取任务并执行切分/去字幕/合成"
9
9
  readme = "README.txt"
10
10
  requires-python = ">=3.8"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: videoconverter-worker
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: VideoConverter Python Worker:从 queue 目录读取任务并执行切分/去字幕/合成
5
5
  License: MIT
6
6
  Keywords: videoconverter,ffmpeg,worker,video
@@ -146,6 +146,45 @@ def check_and_create_merge_task(store: QueueStore, video_id: str, output_dir: st
146
146
  logger.info("自动创建合成任务: videoId=%s, 已处理 %d/%d 块", video_id, len(processed), total)
147
147
 
148
148
 
149
+ def _format_duration(sec: float) -> str:
150
+ """不足1分钟用秒,超过60秒用分钟,超过60分钟用小时。"""
151
+ if sec < 60:
152
+ return f"{sec:.1f}秒" if sec != int(sec) else f"{int(sec)}秒"
153
+ if sec < 3600:
154
+ m = int(sec // 60)
155
+ s = int(round(sec % 60))
156
+ return f"{m}分{s}秒" if s else f"{m}分"
157
+ h = int(sec // 3600)
158
+ m = int((sec % 3600) // 60)
159
+ s = int(round(sec % 60))
160
+ if m and s:
161
+ return f"{h}小时{m}分{s}秒"
162
+ if m:
163
+ return f"{h}小时{m}分"
164
+ if s:
165
+ return f"{h}小时{s}秒"
166
+ return f"{h}小时"
167
+
168
+
169
+ def _log_split_to_merge_duration(data: dict, task_id: str, store: QueueStore) -> None:
170
+ """若 metadata 含 splitStartedAt,则计算并输出从切分到合成结束的总时长。"""
171
+ s = data.get("splitStartedAt") or ""
172
+ if not s:
173
+ return
174
+ try:
175
+ from datetime import datetime, timezone
176
+ ts = datetime.fromisoformat(s.replace("Z", "+00:00"))
177
+ if ts.tzinfo is None:
178
+ ts = ts.replace(tzinfo=timezone.utc)
179
+ duration_sec = (datetime.now(timezone.utc) - ts).total_seconds()
180
+ if duration_sec >= 0:
181
+ msg = f"从切分到合成结束总时长: {_format_duration(duration_sec)}"
182
+ logger.info("videoId=%s %s", data.get("videoId", ""), msg)
183
+ store.add_log(task_id, "INFO", msg)
184
+ except Exception:
185
+ pass
186
+
187
+
149
188
  def process_merge_task(store: QueueStore, task: dict) -> None:
150
189
  task_id = task["task_id"]
151
190
  video_id = task.get("video_id")
@@ -174,6 +213,8 @@ def process_merge_task(store: QueueStore, task: dict) -> None:
174
213
  merge_chunks(data, start_time, end_time, str(output_file))
175
214
  store.complete_task(task_id)
176
215
  store.add_log(task_id, "INFO", f"合成完成: {output_file.name}")
216
+ # 从切分到合成结束总时长(若 metadata 含 splitStartedAt)
217
+ _log_split_to_merge_duration(data, task_id, store)
177
218
  except Exception as e:
178
219
  store.fail_task(task_id, str(e))
179
220
  store.add_log(task_id, "WARN", str(e))
@@ -286,6 +327,7 @@ def run_simple_compose(
286
327
  }
287
328
 
288
329
  logger.info("简易模式: 切分 %s (%.0f - %.0f秒), 字幕高度(裁底)=%d", input_path.name, start_sec, end_sec, crop_bottom)
330
+ t0 = time.time()
289
331
  metadata, video_id = split_video_to_chunks(input_file, output_dir, 120.0, start_sec, end_sec)
290
332
  chunk_list = [c for c in (metadata.get("chunks") or []) if c.get("originalPath")]
291
333
  logger.info("切分完成: %d 块,开始去字幕", len(chunk_list))
@@ -320,7 +362,8 @@ def run_simple_compose(
320
362
  end_t = processed[-1]["endTime"]
321
363
  out_file = Path(output_dir) / f"{video_id}_merged.mp4"
322
364
  merge_chunks(data, start_t, end_t, str(out_file))
323
- logger.info("简易模式完成: %s", out_file)
365
+ elapsed = time.time() - t0
366
+ logger.info("简易模式完成: %s,从切分到合成结束总时长: %s", out_file, _format_duration(elapsed))
324
367
  return str(out_file)
325
368
 
326
369