zziC7 commited on
Commit
ec052d4
·
verified ·
1 Parent(s): 79520d2

Update pitch_animation.py

Browse files
Files changed (1) hide show
  1. pitch_animation.py +30 -49
pitch_animation.py CHANGED
@@ -3,7 +3,7 @@ import re
3
  import sys
4
  import numpy as np
5
  import matplotlib
6
- # 强制使用 Agg 后端,确保云端环境渲染稳定
7
  matplotlib.use('Agg')
8
  import matplotlib.pyplot as plt
9
  import matplotlib.animation as animation
@@ -19,7 +19,6 @@ class PitchAnimationGenerator:
19
  self.output_dir = output_video_dir
20
  self.fps = 15
21
  self.dpi = 100
22
- # 获取系统字体
23
  self.zh_font = self._get_system_font()
24
 
25
  # 音名映射表 (MIDI 0-127)
@@ -28,7 +27,6 @@ class PitchAnimationGenerator:
28
 
29
  os.makedirs(self.output_dir, exist_ok=True)
30
 
31
- # 数据加载流水线
32
  self.summary_segments = self._parse_summary_report()
33
  self.all_pitch_info = self._parse_pitch_srt()
34
  self.pitch_segments = self._match_segment_and_pitch()
@@ -38,7 +36,6 @@ class PitchAnimationGenerator:
38
  linux_path = '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc'
39
  mac_path = '/System/Library/Fonts/PingFang.ttc'
40
  win_path = 'C:/Windows/Fonts/msyh.ttc'
41
-
42
  if os.path.exists(linux_path): return FontProperties(fname=linux_path, size=10)
43
  if os.path.exists(mac_path): return FontProperties(fname=mac_path, size=10)
44
  if os.path.exists(win_path): return FontProperties(fname=win_path, size=10)
@@ -48,8 +45,7 @@ class PitchAnimationGenerator:
48
  try:
49
  h, m, s = t.replace(',', '.').split(':')
50
  return int(h)*3600 + int(m)*60 + float(s)
51
- except:
52
- return 0.0
53
 
54
  def _note_to_midi(self, note_str):
55
  if not note_str: return 60
@@ -69,13 +65,11 @@ class PitchAnimationGenerator:
69
  if match:
70
  t_range = match.group(2)
71
  times = t_range.split(' --> ')
72
- start, end = self._time_to_sec(times[0]), self._time_to_sec(times[1])
73
  segments.append({
74
  'seq_num': int(match.group(1)),
75
  'time_range': t_range,
76
- 'start_sec': start,
77
- 'end_sec': end,
78
- 'duration': end - start
79
  })
80
  except: pass
81
  return segments
@@ -83,15 +77,12 @@ class PitchAnimationGenerator:
83
  def _parse_pitch_srt(self):
84
  pitch_info = []
85
  try:
86
- if not os.path.exists(self.pitch_srt_path): return []
87
  with open(self.pitch_srt_path, 'r', encoding='utf-8') as f:
88
  content = f.read().strip().split('\n\n')
89
  for block in content:
90
  lines = [l.strip() for l in block.split('\n') if l.strip()]
91
  if len(lines) >= 3:
92
- t_range = lines[1]
93
- if ' --> ' not in t_range: continue
94
- times = t_range.split(' --> ')
95
  orig = re.search(r'原唱: ([A-G]#?\d+)', lines[2])
96
  cov = re.search(r'翻唱: ([A-G]#?\d+)', lines[2])
97
  if orig and cov:
@@ -108,9 +99,8 @@ class PitchAnimationGenerator:
108
  for seg in self.summary_segments:
109
  seg['matched_pitches'] = [
110
  p for p in self.all_pitch_info
111
- if 'end_sec' in p and \
112
- p['start_sec'] >= seg['start_sec'] - 0.2 and \
113
- p['end_sec'] <= seg['end_sec'] + 0.5
114
  ]
115
  return self.summary_segments
116
 
@@ -118,20 +108,14 @@ class PitchAnimationGenerator:
118
  audio_map = {}
119
  if not os.path.exists(self.cover_audio_dir): return audio_map
120
  for f in os.listdir(self.cover_audio_dir):
121
- match = re.search(r'句段(\d+)', f)
122
- if match:
123
- seq = int(match.group(1))
124
- audio_map[seq] = os.path.join(self.cover_audio_dir, f)
125
  return audio_map
126
 
127
  def generate_single_animation(self, segment):
128
  seq_num = segment['seq_num']
129
  duration = segment['duration']
130
-
131
- # --- 核心修改:不足1秒直接跳过 ---
132
- if duration < 1.0:
133
- print(f"⚠️ 句段 {seq_num} 长度不足1秒 ({duration:.2f}s),跳过动画生成。")
134
- return None
135
 
136
  pitches = segment['matched_pitches']
137
  if not pitches: return None
@@ -139,33 +123,27 @@ class PitchAnimationGenerator:
139
  all_midis = [p['orig_midi'] for p in pitches] + [p['cov_midi'] for p in pitches]
140
  y_min, y_max = int(min(all_midis)) - 1, int(max(all_midis)) + 1
141
 
142
- # 调整布局,为图表上方留出文字空间
143
- fig, ax = plt.subplots(figsize=(10, 6), dpi=self.dpi)
144
- plt.subplots_adjust(top=0.85) # 留出顶部 15% 的高度
145
 
146
  ax.set_xlim(0, duration)
147
  ax.set_ylim(y_min, y_max)
148
 
149
- # 纵轴音名展示
150
  y_ticks = np.arange(y_min, y_max + 1)
151
  ax.set_yticks(y_ticks)
152
  ax.set_yticklabels([self.midi_to_name[int(y)] for y in y_ticks], fontproperties=self.zh_font)
153
 
154
- ax.set_title(f"句段 {seq_num} 音高对比轨迹", fontproperties=self.zh_font, pad=20)
155
- ax.set_xlabel("时间 (秒)", fontproperties=self.zh_font)
156
- ax.set_ylabel("音高", fontproperties=self.zh_font)
157
- ax.grid(True, linestyle='--', alpha=0.4)
158
-
159
- # --- 核心修改:信息面板移至图表外部 (Fig Title Area) ---
160
- time_info = (f"句段:{seq_num} | 起始时间:{segment['time_range'].split(' --> ')[0]} "
161
- f"| 结束时间:{segment['time_range'].split(' --> ')[1]}")
162
 
163
- # 使用 fig.text 在画布顶部中央居中显示信息
164
- fig.text(0.5, 0.92, time_info, fontproperties=self.zh_font,
165
- fontsize=11, ha='center', fontweight='bold',
166
- bbox=dict(boxstyle='round,pad=0.5', facecolor='#f0f0f0', alpha=0.8, edgecolor='gray'))
167
 
168
- # 1. 绘制原唱参考线 (蓝)
169
  orig_x, orig_y = [], []
170
  for p in pitches:
171
  ps, pe = p['start_sec'] - segment['start_sec'], p['end_sec'] - segment['start_sec']
@@ -173,10 +151,15 @@ class PitchAnimationGenerator:
173
  orig_y.extend([p['orig_midi'], p['orig_midi'], None])
174
  ax.plot(orig_x, orig_y, color='#4169E1', lw=5, alpha=0.25, label='原唱参考')
175
 
176
- # 2. 初始化翻唱线 (绿/红)
177
  match_line, = ax.plot([], [], color='#32CD32', lw=3, label='翻唱准确')
178
  mismatch_line, = ax.plot([], [], color='#DC143C', lw=3, label='翻唱走音')
179
- ax.legend(prop=self.zh_font, loc='upper right', framealpha=0.5)
 
 
 
 
 
180
 
181
  frames = int(duration * self.fps)
182
  t_axis = np.linspace(0, duration, frames)
@@ -185,8 +168,7 @@ class PitchAnimationGenerator:
185
  curr_t = t_axis[frame]
186
  mx, my, ex, ey = [], [], [], []
187
  for p in pitches:
188
- ps = p['start_sec'] - segment['start_sec']
189
- pe = p['end_sec'] - segment['start_sec']
190
  if ps > curr_t: continue
191
  display_pe = min(pe, curr_t)
192
  is_match = abs(p['orig_midi'] - p['cov_midi']) < 0.5
@@ -214,8 +196,7 @@ class PitchAnimationGenerator:
214
  try:
215
  video = VideoFileClip(video_p)
216
  audio = AudioFileClip(audio_p).subclip(0, video.duration)
217
- final = video.set_audio(audio)
218
- final.write_videofile(
219
  out_p, codec="libx264", audio_codec="aac",
220
  logger=None, threads=1, preset="ultrafast",
221
  ffmpeg_params=["-pix_fmt", "yuv420p"]
 
3
  import sys
4
  import numpy as np
5
  import matplotlib
6
+ # 强制使用 Agg 后端
7
  matplotlib.use('Agg')
8
  import matplotlib.pyplot as plt
9
  import matplotlib.animation as animation
 
19
  self.output_dir = output_video_dir
20
  self.fps = 15
21
  self.dpi = 100
 
22
  self.zh_font = self._get_system_font()
23
 
24
  # 音名映射表 (MIDI 0-127)
 
27
 
28
  os.makedirs(self.output_dir, exist_ok=True)
29
 
 
30
  self.summary_segments = self._parse_summary_report()
31
  self.all_pitch_info = self._parse_pitch_srt()
32
  self.pitch_segments = self._match_segment_and_pitch()
 
36
  linux_path = '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc'
37
  mac_path = '/System/Library/Fonts/PingFang.ttc'
38
  win_path = 'C:/Windows/Fonts/msyh.ttc'
 
39
  if os.path.exists(linux_path): return FontProperties(fname=linux_path, size=10)
40
  if os.path.exists(mac_path): return FontProperties(fname=mac_path, size=10)
41
  if os.path.exists(win_path): return FontProperties(fname=win_path, size=10)
 
45
  try:
46
  h, m, s = t.replace(',', '.').split(':')
47
  return int(h)*3600 + int(m)*60 + float(s)
48
+ except: return 0.0
 
49
 
50
  def _note_to_midi(self, note_str):
51
  if not note_str: return 60
 
65
  if match:
66
  t_range = match.group(2)
67
  times = t_range.split(' --> ')
 
68
  segments.append({
69
  'seq_num': int(match.group(1)),
70
  'time_range': t_range,
71
+ 'start_sec': self._time_to_sec(times[0]),
72
+ 'duration': self._time_to_sec(times[1]) - self._time_to_sec(times[0])
 
73
  })
74
  except: pass
75
  return segments
 
77
  def _parse_pitch_srt(self):
78
  pitch_info = []
79
  try:
 
80
  with open(self.pitch_srt_path, 'r', encoding='utf-8') as f:
81
  content = f.read().strip().split('\n\n')
82
  for block in content:
83
  lines = [l.strip() for l in block.split('\n') if l.strip()]
84
  if len(lines) >= 3:
85
+ times = lines[1].split(' --> ')
 
 
86
  orig = re.search(r'原唱: ([A-G]#?\d+)', lines[2])
87
  cov = re.search(r'翻唱: ([A-G]#?\d+)', lines[2])
88
  if orig and cov:
 
99
  for seg in self.summary_segments:
100
  seg['matched_pitches'] = [
101
  p for p in self.all_pitch_info
102
+ if 'end_sec' in p and p['start_sec'] >= seg['start_sec'] - 0.2
103
+ and p['end_sec'] <= seg['start_sec'] + seg['duration'] + 0.5
 
104
  ]
105
  return self.summary_segments
106
 
 
108
  audio_map = {}
109
  if not os.path.exists(self.cover_audio_dir): return audio_map
110
  for f in os.listdir(self.cover_audio_dir):
111
+ m = re.search(r'句段(\d+)', f)
112
+ if m: audio_map[int(m.group(1))] = os.path.join(self.cover_audio_dir, f)
 
 
113
  return audio_map
114
 
115
  def generate_single_animation(self, segment):
116
  seq_num = segment['seq_num']
117
  duration = segment['duration']
118
+ if duration < 1.0: return None # 跳过不足1秒
 
 
 
 
119
 
120
  pitches = segment['matched_pitches']
121
  if not pitches: return None
 
123
  all_midis = [p['orig_midi'] for p in pitches] + [p['cov_midi'] for p in pitches]
124
  y_min, y_max = int(min(all_midis)) - 1, int(max(all_midis)) + 1
125
 
126
+ # 设置画布,增加右侧宽度比例以容纳外部图例
127
+ fig, ax = plt.subplots(figsize=(12, 6), dpi=self.dpi)
 
128
 
129
  ax.set_xlim(0, duration)
130
  ax.set_ylim(y_min, y_max)
131
 
 
132
  y_ticks = np.arange(y_min, y_max + 1)
133
  ax.set_yticks(y_ticks)
134
  ax.set_yticklabels([self.midi_to_name[int(y)] for y in y_ticks], fontproperties=self.zh_font)
135
 
136
+ # --- 核心修改:两行标题展示 ---
137
+ start_time = segment['time_range'].split(' --> ')[0]
138
+ end_time = segment['time_range'].split(' --> ')[1]
139
+ full_title = f"句段 {seq_num} 音高对比轨迹\n起止时间: {start_time} --> {end_time}"
140
+ ax.set_title(full_title, fontproperties=self.zh_font, fontsize=12, pad=15)
 
 
 
141
 
142
+ ax.set_xlabel("相对时间 (秒)", fontproperties=self.zh_font)
143
+ ax.set_ylabel("音高名", fontproperties=self.zh_font)
144
+ ax.grid(True, linestyle='--', alpha=0.4)
 
145
 
146
+ # 1. 绘制原唱参考线
147
  orig_x, orig_y = [], []
148
  for p in pitches:
149
  ps, pe = p['start_sec'] - segment['start_sec'], p['end_sec'] - segment['start_sec']
 
151
  orig_y.extend([p['orig_midi'], p['orig_midi'], None])
152
  ax.plot(orig_x, orig_y, color='#4169E1', lw=5, alpha=0.25, label='原唱参考')
153
 
154
+ # 2. 初始化翻唱线
155
  match_line, = ax.plot([], [], color='#32CD32', lw=3, label='翻唱准确')
156
  mismatch_line, = ax.plot([], [], color='#DC143C', lw=3, label='翻唱走音')
157
+
158
+ # --- 核心修改:图例移至右侧外部 ---
159
+ ax.legend(prop=self.zh_font, loc='upper left', bbox_to_anchor=(1.02, 1), borderaxespad=0.)
160
+
161
+ # 调整整体布局,为右侧图例和顶部双行标题留空
162
+ plt.tight_layout(rect=[0, 0, 0.85, 1])
163
 
164
  frames = int(duration * self.fps)
165
  t_axis = np.linspace(0, duration, frames)
 
168
  curr_t = t_axis[frame]
169
  mx, my, ex, ey = [], [], [], []
170
  for p in pitches:
171
+ ps, pe = p['start_sec'] - segment['start_sec'], p['end_sec'] - segment['start_sec']
 
172
  if ps > curr_t: continue
173
  display_pe = min(pe, curr_t)
174
  is_match = abs(p['orig_midi'] - p['cov_midi']) < 0.5
 
196
  try:
197
  video = VideoFileClip(video_p)
198
  audio = AudioFileClip(audio_p).subclip(0, video.duration)
199
+ video.set_audio(audio).write_videofile(
 
200
  out_p, codec="libx264", audio_codec="aac",
201
  logger=None, threads=1, preset="ultrafast",
202
  ffmpeg_params=["-pix_fmt", "yuv420p"]