《回文·花月》:AI回文诗歌曲MV制作流程


本作品围绕“寒月对影”的意象,创作了一首回文诗,结合虚拟歌姬演唱与AI生成伴奏、动态画面,完成了一部风格独特的古风音乐视频。整个过程融合了 GPT 文本生成、Python 作曲、XStudio 歌声合成、Trae AI辅助代码与 Sora 画面生成技术。


🎼 最终成品

🎼 视频制作流程

🎼 图文制作流程

✒️ 1. GPT生成歌词与初版伴奏(Python)

  • 利用 ChatGPT 编写古风回文诗句。

  • 📝 回文歌词展示

1
2
3
4
5
6
7
8
9
月寒风静,花落云轻。  
山遥水阔,夜静星明。
琴幽梦远,雁影烟平。
心闲道浅,意远情清。

清情远意,浅道闲心。
平烟影雁,远梦幽琴。
明星静夜,阔水遥山。
轻云落花,静风寒月。
  • 使用 Python(mido)生成基础伴奏 MIDI 文件。
  • 初版节奏为 4/4 拍,60 BPM,乐器选择以钢琴为主,加入气口。
  • 📝示例代码片段如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# filename: generate_poem_motif_with_rest.py

from mido import MidiFile, MidiTrack, Message, MetaMessage, bpm2tempo

# ────────── 可调节参数 ──────────
TEMPO_BPM = 90
VELOCITY = 70
BASE_NOTE = 60 # C4
GROUPS = 5 # 共5组动机
TICKS_PER_BEAT = 480 # 默认分辨率
# 原始动机(4小节,每字1拍)
MOTIF = [
[60, 62, 64, 67], # 月寒风静
[67, 64, 62, 60], # 花落云轻
[60, 64, 67, 69], # 山遥水阔
[69, 67, 64, 62], # 夜静星明
]
# ──────────────────────────────

PENTATONIC_DEGREES = {0, 2, 4, 7, 9}

def in_scale(note: int) -> bool:
return note % 12 in PENTATONIC_DEGREES

def shift_up(note: int, steps: int = 1) -> int:
new_note, moved = note, 0
while moved < steps:
new_note += 1
if in_scale(new_note):
moved += 1
return new_note

def transpose_motif(motif: list[list[int]], steps: int) -> list[list[int]]:
if steps == 0:
return motif
return [[shift_up(n, steps) for n in bar] for bar in motif]

# ────────── MIDI 生成 ──────────
mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)

track.append(MetaMessage('set_tempo', tempo=bpm2tempo(TEMPO_BPM)))
track.append(Message('program_change', program=0, time=0))

one_beat = TICKS_PER_BEAT
one_measure = 4 * one_beat

for g in range(GROUPS):
# 确定上行模进的阶数
steps = 0 if (g == 0 or g == GROUPS - 1) else g
current = transpose_motif(MOTIF, steps)
for bar in current:
for note in bar:
track.append(Message('note_on', note=note, velocity=VELOCITY, time=0))
track.append(Message('note_off', note=note, velocity=VELOCITY, time=one_beat))
# 每组后加入1小节休止
track.append(Message('note_off', note=0, velocity=0, time=one_measure))

mid.save("five_tone_poem_full_with_rest.mid")
print("✅ 加气口版本已生成:five_tone_poem_full_with_rest.mid")


🎤 2. Trae修改伴奏,X studio虚拟歌姬演唱调试

  • 反复修改代码和伴奏
  • 终版伴奏展示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
from mido import MidiFile, MidiTrack, Message, MetaMessage, bpm2tempo
import os
import subprocess
import tempfile

# ────────── CONFIGURATION ──────────
TEMPO_BPM = 90 # beats per minute
VELOCITY_MELODY = 70 # MIDI velocity for melody
VELOCITY_CHORDS = 50 # MIDI velocity for chords
VELOCITY_DRUMS = 80 # MIDI velocity for drums
GROUPS = 5 # 5 groups of the 16‑syllable motif
INTRO_MEAS = 4 # 4‑bar intro before melody starts (增加前奏长度)
REST_MEAS = 1 # 1‑bar rest between groups (气口)

# Pentatonic scale degrees relative to C
PENTA_DEGREES = {0, 2, 4, 7, 9}
BASE_NOTE = 60 # C4 as 宫

# Original 4‑bar motif (one bar = one four‑character line)
MOTIF = [
[60, 62, 64, 67], # 月寒风静
[67, 64, 62, 60], # 花落云轻
[60, 64, 67, 69], # 山遥水阔
[69, 67, 64, 62], # 夜静星明
]

# Percussion note numbers (GM standard)
KICK = 36 # Acoustic Bass Drum
SNARE = 38 # Acoustic Snare
HIHAT = 42 # Closed Hi‑Hat

# ────────── HELPER FUNCTIONS ──────────

def in_scale(note: int) -> bool:
"""Return True if note belongs to C pentatonic."""
return note % 12 in PENTA_DEGREES

def shift_up(note: int, steps: int = 1) -> int:
"""Shift note up by `steps` pentatonic degrees without leaving the scale."""
moved, n = 0, note
while moved < steps:
n += 1
if in_scale(n):
moved += 1
return n

def transpose_motif(motif, steps: int):
if steps == 0:
return motif
return [[shift_up(n, steps) for n in bar] for bar in motif]

# ────────── 创建两个MIDI文件:伴奏和人声 ──────────
# 伴奏MIDI文件
accompaniment_mid = MidiFile()
# 人声MIDI文件
vocal_mid = MidiFile()

# ────────── 伴奏文件设置 ──────────
# Tempo/Meta track for accompaniment
acc_meta_tr = MidiTrack(); accompaniment_mid.tracks.append(acc_meta_tr)
acc_meta_tr.append(MetaMessage('set_tempo', tempo=bpm2tempo(TEMPO_BPM), time=0))
acc_meta_tr.append(MetaMessage('time_signature', numerator=4, denominator=4, clocks_per_click=24, notated_32nd_notes_per_beat=8, time=0))

# Chord track (channel 1)
chd_tr = MidiTrack(); accompaniment_mid.tracks.append(chd_tr)
chd_tr.append(Message('program_change', channel=1, program=48, time=0)) # Strings Ensemble 1

# 添加前奏旋律轨道 (channel 2)
intro_mel_tr = MidiTrack(); accompaniment_mid.tracks.append(intro_mel_tr)
intro_mel_tr.append(Message('program_change', channel=2, program=73, time=0)) # Flute

# Percussion track (channel 9 = drums)
per_tr = MidiTrack(); accompaniment_mid.tracks.append(per_tr)

# ────────── 人声文件设置 ──────────
# Tempo/Meta track for vocal
voc_meta_tr = MidiTrack(); vocal_mid.tracks.append(voc_meta_tr)
voc_meta_tr.append(MetaMessage('set_tempo', tempo=bpm2tempo(TEMPO_BPM), time=0))
voc_meta_tr.append(MetaMessage('time_signature', numerator=4, denominator=4, clocks_per_click=24, notated_32nd_notes_per_beat=8, time=0))

# Melody track (channel 0)
mel_tr = MidiTrack(); vocal_mid.tracks.append(mel_tr)
mel_tr.append(Message('program_change', channel=0, program=0, time=0)) # Acoustic Grand Piano

PPQ = accompaniment_mid.ticks_per_beat # default 480
BEAT = PPQ # quarter note
BAR = 4 * BEAT # one measure of 4/4

# ────────── BUILD INTRO ──────────
# 前奏旋律 - 简单的五声音阶旋律,为主旋律做铺垫
intro_melody = [
# 第1小节 - 简单上行
[60, 64, 67, 69],
# 第2小节 - 下行
[67, 64, 62, 60],
# 第3小节 - 变化上行
[62, 64, 67, 72],
# 第4小节 - 收束
[69, 67, 64, 60]
]

for m in range(INTRO_MEAS):
# Chords: sustained C‑G‑C (power chord) for full measure
root = BASE_NOTE
chd_tr.append(Message('note_on', channel=1, note=root, velocity=VELOCITY_CHORDS, time=0))
chd_tr.append(Message('note_on', channel=1, note=root+7, velocity=VELOCITY_CHORDS, time=0))
chd_tr.append(Message('note_on', channel=1, note=root+12,velocity=VELOCITY_CHORDS, time=0))
chd_tr.append(Message('note_off',channel=1, note=root, velocity=0, time=BAR))
chd_tr.append(Message('note_off',channel=1, note=root+7, velocity=0, time=0))
chd_tr.append(Message('note_off',channel=1, note=root+12,velocity=0, time=0))

# 添加前奏旋律
for note_idx, note in enumerate(intro_melody[m]):
# 使用稍微轻柔的力度
intro_mel_tr.append(Message('note_on', channel=2, note=note, velocity=VELOCITY_MELODY-10, time=0))
# 每个音符持续一拍
intro_mel_tr.append(Message('note_off', channel=2, note=note, velocity=0, time=BEAT))

# Percussion pattern (kick‑snare alternating, hihat every beat)
for beat in range(4):
if beat == 0 or beat == 2:
per_tr.append(Message('note_on', channel=9, note=KICK, velocity=VELOCITY_DRUMS, time=0))
per_tr.append(Message('note_off',channel=9, note=KICK, velocity=0, time=BEAT//2))
else:
per_tr.append(Message('note_on', channel=9, note=SNARE, velocity=VELOCITY_DRUMS, time=0))
per_tr.append(Message('note_off',channel=9, note=SNARE, velocity=0, time=BEAT//2))
# Hi‑hat overlay
per_tr.append(Message('note_on', channel=9, note=HIHAT, velocity=VELOCITY_DRUMS//2, time=0))
per_tr.append(Message('note_off',channel=9, note=HIHAT, velocity=0, time=BEAT//2))

# 人声轨道需要等待前奏结束
mel_tr.append(Message('note_off', channel=0, note=0, velocity=0, time=INTRO_MEAS * BAR))

# ────────── BUILD 5 GROUPS (MELODY + HARMONY + PERC) ──────────
for g in range(GROUPS):
# Determine pentatonic shift (0, +1, +2, +3, 0)
steps = 0 if (g == 0 or g == GROUPS - 1) else g
motif = transpose_motif(MOTIF, steps)

# For every bar in motif
for bar_idx, bar_notes in enumerate(motif):
# Melody — four notes, each one beat (只添加到人声文件)
for n in bar_notes:
mel_tr.append(Message('note_on', channel=0, note=n, velocity=VELOCITY_MELODY, time=0))
mel_tr.append(Message('note_off',channel=0, note=n, velocity=0, time=BEAT))

# Chord root is the 1st melody note of the bar (添加到伴奏文件)
root = bar_notes[0]
chd_tr.append(Message('note_on', channel=1, note=root, velocity=VELOCITY_CHORDS, time=0))
chd_tr.append(Message('note_on', channel=1, note=root+7, velocity=VELOCITY_CHORDS, time=0))
chd_tr.append(Message('note_on', channel=1, note=root+12,velocity=VELOCITY_CHORDS, time=0))
chd_tr.append(Message('note_off',channel=1, note=root, velocity=0, time=BAR))
chd_tr.append(Message('note_off',channel=1, note=root+7, velocity=0, time=0))
chd_tr.append(Message('note_off',channel=1, note=root+12,velocity=0, time=0))

# Percussion for the bar (添加到伴奏文件)
for beat in range(4):
drum_note = KICK if beat in (0, 2) else SNARE
per_tr.append(Message('note_on', channel=9, note=drum_note, velocity=VELOCITY_DRUMS, time=0))
per_tr.append(Message('note_off',channel=9, note=drum_note, velocity=0, time=BEAT//2))
# Hi‑hat overlay
per_tr.append(Message('note_on', channel=9, note=HIHAT, velocity=VELOCITY_DRUMS//2, time=0))
per_tr.append(Message('note_off',channel=9, note=HIHAT, velocity=0, time=BEAT//2))

# Add REST_MEAS rest (气口) between groups (skip after last group)
if g < GROUPS - 1:
# 人声轨道暂停
mel_tr.append(Message('note_off', channel=0, note=0, velocity=0, time=REST_MEAS * BAR))

# 改进气口部分的旋律,使其更加优美自然
# 根据当前小节的最后一个音符创建一个更加优美的过渡旋律
last_note = motif[-1][-1] # 获取当前组最后一个音符

# 创建更加优美的气口旋律 - 使用五声音阶中的音符
breath_melody = []
if g % 2 == 0:
# 偶数组后的气口 - 上行旋律
breath_melody = [last_note-5, last_note-2, last_note, last_note+2]
else:
# 奇数组后的气口 - 下行旋律
breath_melody = [last_note+2, last_note, last_note-2, last_note-5]

# 确保所有音符都在五声音阶中
breath_melody = [note for note in breath_melody if in_scale(note)]

# 添加更优美的气口旋律
for i, note in enumerate(breath_melody):
intro_mel_tr.append(Message('note_on', channel=2, note=note, velocity=VELOCITY_MELODY-5, time=0))
# 使用更长的音符持续时间,创造更流畅的旋律
intro_mel_tr.append(Message('note_off', channel=2, note=note, velocity=0, time=BEAT//2))

# 和弦继续 - 使用更丰富的和弦
chd_tr.append(Message('note_on', channel=1, note=root-12, velocity=VELOCITY_CHORDS-5, time=0))
chd_tr.append(Message('note_on', channel=1, note=root-5, velocity=VELOCITY_CHORDS-5, time=0))
chd_tr.append(Message('note_on', channel=1, note=root+4, velocity=VELOCITY_CHORDS-5, time=0))
chd_tr.append(Message('note_off',channel=1, note=root-12, velocity=0, time=REST_MEAS * BAR))
chd_tr.append(Message('note_off',channel=1, note=root-5, velocity=0, time=0))
chd_tr.append(Message('note_off',channel=1, note=root+4, velocity=0, time=0))

# 打击乐继续 - 使用更丰富的节奏模式
for beat in range(4 * REST_MEAS):
if beat % 4 == 0:
drum_note = KICK
velocity = VELOCITY_DRUMS-5
elif beat % 4 == 2:
drum_note = SNARE
velocity = VELOCITY_DRUMS-10
else:
drum_note = HIHAT
velocity = VELOCITY_DRUMS-15

per_tr.append(Message('note_on', channel=9, note=drum_note, velocity=velocity, time=0))
per_tr.append(Message('note_off',channel=9, note=drum_note, velocity=0, time=BEAT//2))

# ────────── SAVE FILES ──────────
ACCOMPANIMENT_MIDI = "poem_accompaniment.mid"
VOCAL_MIDI = "poem_vocal.mid"

# 保存MIDI文件
accompaniment_mid.save(ACCOMPANIMENT_MIDI)
vocal_mid.save(VOCAL_MIDI)
print(f"✅ 伴奏MIDI文件已生成: {ACCOMPANIMENT_MIDI}")
print(f"✅ 人声MIDI文件已生成: {VOCAL_MIDI}")
  • 保存音频,加入X studio中对齐伴奏和人声
  • 调整虚拟歌姬的咬字与旋律
  • 不断重复上述步骤直到满意

🌌 4. Sora生成古风循环动态画面

  • 使用 Sora 根据关键词 “夜色/花落/月影/古风水面” 生成动态场景。
  • 控制画面为 15s循环,与音乐拍点对齐:
    • 第一节:花瓣飘落
    • 第二节:水中倒影波动
    • 第三节:远山与云光慢慢流动

🌀 示例 prompt:

1
泛着波光的湖面上,远山隐约可见,画面整体温柔、留白、古典,适合用作古风音乐视频背景。循环播放、静谧优雅.