sungo-ganpare commited on
Commit
a89d03e
·
1 Parent(s): 2311e41

ffmpegを使用して大容量音声ファイルの処理を改善し、音声長の取得機能を追加

Browse files
Files changed (3) hide show
  1. .dockerignore +37 -0
  2. Dockerfile +26 -0
  3. transcribe_cli.py +157 -13
.dockerignore ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Git
2
+ .git
3
+ .gitignore
4
+
5
+ # Python
6
+ __pycache__
7
+ *.pyc
8
+ *.pyo
9
+ *.pyd
10
+ .Python
11
+ env
12
+ pip-log.txt
13
+ pip-delete-this-directory.txt
14
+ .tox
15
+ .coverage
16
+ .coverage.*
17
+ .cache
18
+ nosetests.xml
19
+ coverage.xml
20
+ *.cover
21
+ *.log
22
+
23
+ # Virtual Environment
24
+ .env
25
+ .venv
26
+ venv/
27
+ ENV/
28
+
29
+ # Output directories
30
+ outputs/
31
+ data/
32
+
33
+ # IDE specific files
34
+ .idea
35
+ .vscode
36
+ *.swp
37
+ *.swo
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use NVIDIA CUDA base image
2
+ FROM nvidia/cuda:11.8.0-runtime-ubuntu22.04
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y \
9
+ python3 \
10
+ python3-pip \
11
+ git \
12
+ ffmpeg \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # Copy only the necessary files
16
+ COPY requirements.txt .
17
+ COPY transcribe_cli.py .
18
+
19
+ # Install Python dependencies
20
+ RUN pip3 install --no-cache-dir -r requirements.txt
21
+
22
+ # Set environment variable for CUDA device
23
+ ENV CUDA_VISIBLE_DEVICES=0
24
+
25
+ # Set default command
26
+ ENTRYPOINT ["python3", "transcribe_cli.py", "/app/data"]
transcribe_cli.py CHANGED
@@ -13,6 +13,8 @@ import argparse
13
  import time # ★処理時間計測のために追加
14
  import sys # ★コマンドライン引数チェックのために追加
15
  from nemo.collections.asr.models import ASRModel # NeMo ASRモデル
 
 
16
 
17
  # --- グローバル設定 ---
18
  MODEL_NAME = "nvidia/parakeet-tdt-0.6b-v2"
@@ -42,14 +44,39 @@ def preprocess_audio_cli(audio_path_str: str, output_dir_for_temp_files: str) ->
42
  audio_name_stem = audio_file_path.stem
43
 
44
  print(f" 音声ファイルをロード中: {original_path_name}")
45
- audio = AudioSegment.from_file(audio_path_str)
46
- duration_sec = audio.duration_seconds
 
 
 
 
 
47
  print(f" 音声長: {duration_sec:.2f} 秒")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  except FileNotFoundError:
50
  print(f"エラー: 音声ファイルが見つかりません: {audio_path_str}")
51
  return None, None, None
52
- except Exception as load_e: # pydub.exceptions.CouldntDecodeError などを含む
53
  print(f"エラー: 音声ファイル '{original_path_name}' のロード/デコードに失敗しました: {load_e}")
54
  return None, None, None
55
 
@@ -81,24 +108,28 @@ def preprocess_audio_cli(audio_path_str: str, output_dir_for_temp_files: str) ->
81
  elif audio.channels == 1:
82
  print(" 音声は既にモノラルです。")
83
 
84
- processed_temp_file_path_obj = None # 一時ファイルのPathオブジェクト
85
  # 前処理が行われた場合、一時ファイルに保存
86
  if resampled or mono_converted:
87
  try:
88
- temp_suffix = "_preprocessed_temp.wav" # 一時ファイルとわかるような接尾辞
89
- processed_temp_file_path_obj = Path(output_dir_for_temp_files, f"{audio_name_stem}{temp_suffix}")
 
 
 
90
 
91
  print(f" 前処理済み音声の一時保存先: {processed_temp_file_path_obj.name}")
92
  audio.export(processed_temp_file_path_obj, format="wav")
93
 
94
- path_for_transcription = processed_temp_file_path_obj.as_posix() # 文字起こしに使用するパス
95
  display_name_for_info = f"{original_path_name} (前処理済み)"
96
  except Exception as export_e:
97
  print(f"エラー: 前処理済み音声のエクスポートに失敗しました: {export_e}")
98
- # エクスポート失敗時、もしファイルが作られていたら削除試行
99
  if processed_temp_file_path_obj and processed_temp_file_path_obj.exists():
100
- try: os.remove(processed_temp_file_path_obj)
101
- except OSError: pass # 削除エラーはここでは致命的ではない
 
 
102
  return None, None, None
103
  else:
104
  # 前処理が不要だった場合
@@ -108,6 +139,33 @@ def preprocess_audio_cli(audio_path_str: str, output_dir_for_temp_files: str) ->
108
 
109
  return path_for_transcription, display_name_for_info, duration_sec
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  # --- 文字起こしコア関数 ---
113
  def transcribe_audio_cli(
@@ -373,10 +431,28 @@ def split_audio_with_overlap_cli(
373
  overlap_sec: int = CHUNK_OVERLAP_SECONDS
374
  ) -> List[str]:
375
  print(f" 音声分割中: 基本チャンク長 {chunk_length_sec}s, オーバーラップ {overlap_sec}s")
376
- try: audio = AudioSegment.from_file(audio_path_str)
 
 
 
 
 
 
 
 
 
 
 
 
377
  except Exception as e:
378
- print(f" エラー: 音声ファイル '{Path(audio_path_str).name}' のロード中にエラー(分割処理): {e}")
379
- return []
 
 
 
 
 
 
380
  duration_ms = len(audio); chunk_length_ms = chunk_length_sec * 1000; overlap_ms = overlap_sec * 1000
381
  chunk_paths_list: List[str] = []; start_ms = 0; chunk_idx = 0
382
  audio_file_stem = Path(audio_path_str).stem
@@ -400,6 +476,74 @@ def split_audio_with_overlap_cli(
400
  print(f" 音声を {len(chunk_paths_list)} 個のチャンクに分割しました。")
401
  return chunk_paths_list
402
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  # --- 単一ファイル処理のメインロジック ---
404
  def process_single_file(
405
  input_file_path_obj: Path,
 
13
  import time # ★処理時間計測のために追加
14
  import sys # ★コマンドライン引数チェックのために追加
15
  from nemo.collections.asr.models import ASRModel # NeMo ASRモデル
16
+ import subprocess
17
+ import shutil
18
 
19
  # --- グローバル設定 ---
20
  MODEL_NAME = "nvidia/parakeet-tdt-0.6b-v2"
 
44
  audio_name_stem = audio_file_path.stem
45
 
46
  print(f" 音声ファイルをロード中: {original_path_name}")
47
+
48
+ # まずffprobeで音声長を取得(4GB制限なし)
49
+ duration_sec = get_audio_duration_with_ffprobe(audio_path_str)
50
+ if duration_sec is None:
51
+ print("エラー: ffprobeで音声長の取得に失敗しました")
52
+ return None, None, None
53
+
54
  print(f" 音声長: {duration_sec:.2f} 秒")
55
+
56
+ # ファイルサイズをチェック
57
+ file_size = Path(audio_path_str).stat().st_size
58
+ file_size_gb = file_size / (1024**3)
59
+ print(f" ファイルサイズ: {file_size_gb:.2f} GB")
60
+
61
+ # 4GB以上またはVERY_LONG_AUDIO_THRESHOLD_SECONDS以上の場合は直接ffmpegでチャンク分割
62
+ if file_size > 4 * 1024**3 or duration_sec > VERY_LONG_AUDIO_THRESHOLD_SECONDS:
63
+ print(f" 大容量ファイル({file_size_gb:.2f}GB, {duration_sec/3600:.2f}時間)のため、ffmpegで直接チャンク分割処理を行います。")
64
+ return audio_path_str, f"{original_path_name} (大容量)", duration_sec
65
+
66
+ # 4GB未満の場合は従来のpydub処理
67
+ try:
68
+ audio = AudioSegment.from_file(audio_path_str)
69
+ except Exception as pydub_e:
70
+ if "4GB" in str(pydub_e) or "Unable to process" in str(pydub_e):
71
+ print(f" pydubで4GB制限エラー。ffmpegで処理します: {pydub_e}")
72
+ return audio_path_str, f"{original_path_name} (大容量)", duration_sec
73
+ else:
74
+ raise pydub_e
75
 
76
  except FileNotFoundError:
77
  print(f"エラー: 音声ファイルが見つかりません: {audio_path_str}")
78
  return None, None, None
79
+ except Exception as load_e:
80
  print(f"エラー: 音声ファイル '{original_path_name}' のロード/デコードに失敗しました: {load_e}")
81
  return None, None, None
82
 
 
108
  elif audio.channels == 1:
109
  print(" 音声は既にモノラルです。")
110
 
111
+ processed_temp_file_path_obj = None
112
  # 前処理が行われた場合、一時ファイルに保存
113
  if resampled or mono_converted:
114
  try:
115
+ # ファイル名から特殊文字を除去してより安全な名前を生成
116
+ import re
117
+ safe_stem = re.sub(r'[^\w\-_\.]', '_', audio_name_stem)
118
+ temp_suffix = "_preprocessed_temp.wav"
119
+ processed_temp_file_path_obj = Path(output_dir_for_temp_files, f"{safe_stem}{temp_suffix}")
120
 
121
  print(f" 前処理済み音声の一時保存先: {processed_temp_file_path_obj.name}")
122
  audio.export(processed_temp_file_path_obj, format="wav")
123
 
124
+ path_for_transcription = processed_temp_file_path_obj.as_posix()
125
  display_name_for_info = f"{original_path_name} (前処理済み)"
126
  except Exception as export_e:
127
  print(f"エラー: 前処理済み音声のエクスポートに失敗しました: {export_e}")
 
128
  if processed_temp_file_path_obj and processed_temp_file_path_obj.exists():
129
+ try:
130
+ os.remove(processed_temp_file_path_obj)
131
+ except OSError:
132
+ pass
133
  return None, None, None
134
  else:
135
  # 前処理が不要だった場合
 
139
 
140
  return path_for_transcription, display_name_for_info, duration_sec
141
 
142
+ def get_audio_duration_with_ffprobe(audio_path_str: str) -> Optional[float]:
143
+ """ffprobeを使用して音声ファイルの長さを取得(4GB制限なし)"""
144
+ try:
145
+ # ffprobeが利用可能かチェック
146
+ if not shutil.which('ffprobe'):
147
+ print("警告: ffprobeが見つかりません。pydubでの処理を試行します。")
148
+ return None
149
+
150
+ cmd = [
151
+ 'ffprobe', '-v', 'quiet', '-show_entries', 'format=duration',
152
+ '-of', 'csv=p=0', audio_path_str
153
+ ]
154
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
155
+
156
+ if result.returncode == 0 and result.stdout.strip():
157
+ duration = float(result.stdout.strip())
158
+ return duration
159
+ else:
160
+ print(f"ffprobeエラー: {result.stderr}")
161
+ return None
162
+
163
+ except subprocess.TimeoutExpired:
164
+ print("ffprobeがタイムアウトしました")
165
+ return None
166
+ except Exception as e:
167
+ print(f"ffprobeでの音声長取得エラー: {e}")
168
+ return None
169
 
170
  # --- 文字起こしコア関数 ---
171
  def transcribe_audio_cli(
 
431
  overlap_sec: int = CHUNK_OVERLAP_SECONDS
432
  ) -> List[str]:
433
  print(f" 音声分割中: 基本チャンク長 {chunk_length_sec}s, オーバーラップ {overlap_sec}s")
434
+
435
+ # ファイルサイズをチェックして処理方法を決定
436
+ file_size = Path(audio_path_str).stat().st_size
437
+ file_size_gb = file_size / (1024**3)
438
+
439
+ # 4GB以上の場合はffmpegを使用
440
+ if file_size > 4 * 1024**3:
441
+ print(f" 大容量ファイル({file_size_gb:.2f}GB)のため、ffmpegで分割処理を実行します。")
442
+ return split_audio_with_ffmpeg(audio_path_str, output_dir_for_chunks, chunk_length_sec, overlap_sec)
443
+
444
+ # 4GB未満の場合は従来のpydub処理
445
+ try:
446
+ audio = AudioSegment.from_file(audio_path_str)
447
  except Exception as e:
448
+ if "4GB" in str(e) or "Unable to process" in str(e):
449
+ print(f" pydubで4GB制限エラー。ffmpegで処理します: {e}")
450
+ return split_audio_with_ffmpeg(audio_path_str, output_dir_for_chunks, chunk_length_sec, overlap_sec)
451
+ else:
452
+ print(f" エラー: 音声ファイル '{Path(audio_path_str).name}' のロード中にエラー(分割処理): {e}")
453
+ return []
454
+
455
+ # 以下は既存のpydub処理...
456
  duration_ms = len(audio); chunk_length_ms = chunk_length_sec * 1000; overlap_ms = overlap_sec * 1000
457
  chunk_paths_list: List[str] = []; start_ms = 0; chunk_idx = 0
458
  audio_file_stem = Path(audio_path_str).stem
 
476
  print(f" 音声を {len(chunk_paths_list)} 個のチャンクに分割しました。")
477
  return chunk_paths_list
478
 
479
+ def split_audio_with_ffmpeg(
480
+ audio_path_str: str,
481
+ output_dir_for_chunks: str,
482
+ chunk_length_sec: int,
483
+ overlap_sec: int
484
+ ) -> List[str]:
485
+ """ffmpegを使用して大容量ファイルを分割"""
486
+ try:
487
+ if not shutil.which('ffmpeg'):
488
+ print("エラー: ffmpegが見つかりません。4GB以上のファイルを処理するにはffmpegが必要です。")
489
+ return []
490
+
491
+ # 音声長を取得
492
+ duration_sec = get_audio_duration_with_ffprobe(audio_path_str)
493
+ if duration_sec is None:
494
+ print("エラー: ffmpegでの分割処理で音声長を取得できませんでした")
495
+ return []
496
+
497
+ chunk_paths_list: List[str] = []
498
+ audio_file_stem = Path(audio_path_str).stem
499
+ start_sec = 0
500
+ chunk_idx = 0
501
+
502
+ while start_sec < duration_sec:
503
+ # チャンク開始・終了時刻を計算
504
+ actual_start_sec = max(0, start_sec - (overlap_sec if start_sec > 0 else 0))
505
+ base_end_sec = start_sec + chunk_length_sec
506
+ actual_end_sec = min(base_end_sec + (overlap_sec if base_end_sec < duration_sec else 0), duration_sec)
507
+
508
+ if actual_start_sec >= actual_end_sec:
509
+ break
510
+
511
+ chunk_duration = actual_end_sec - actual_start_sec
512
+ chunk_file_name = f"{audio_file_stem}_chunk_{chunk_idx:03d}_temp.wav"
513
+ chunk_file_path = Path(output_dir_for_chunks) / chunk_file_name
514
+
515
+ # ffmpegコマンドで音声を抽出・変換
516
+ cmd = [
517
+ 'ffmpeg', '-y', '-loglevel', 'error',
518
+ '-ss', str(actual_start_sec),
519
+ '-i', audio_path_str,
520
+ '-t', str(chunk_duration),
521
+ '-acodec', 'pcm_s16le',
522
+ '-ar', str(TARGET_SAMPLE_RATE),
523
+ '-ac', '1', # モノラル
524
+ str(chunk_file_path)
525
+ ]
526
+
527
+ try:
528
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
529
+ if result.returncode == 0:
530
+ chunk_paths_list.append(chunk_file_path.as_posix())
531
+ print(f" チャンク {chunk_idx+1}: {actual_start_sec:.1f}s - {actual_end_sec:.1f}s -> {chunk_file_name}")
532
+ else:
533
+ print(f" エラー: チャンク {chunk_idx} の生成に失敗: {result.stderr}")
534
+ except subprocess.TimeoutExpired:
535
+ print(f" エラー: チャンク {chunk_idx} の生成がタイムアウトしました")
536
+
537
+ start_sec += chunk_length_sec
538
+ chunk_idx += 1
539
+
540
+ print(f" ffmpegで音声を {len(chunk_paths_list)} 個のチャンクに分割しました。")
541
+ return chunk_paths_list
542
+
543
+ except Exception as e:
544
+ print(f" エラー: ffmpegでの音声分割中にエラー: {e}")
545
+ return []
546
+
547
  # --- 単一ファイル処理のメインロジック ---
548
  def process_single_file(
549
  input_file_path_obj: Path,