关于 ttm:agent 对唱逻辑的参考以及个人想法 #8947
Replies: 7 comments
V1 (根据上面的描述所编写的逻辑代码)
main_agent = None
duet_status = False
for agent in agents:
if agent.is_person:
main_agent = agent.id
break
for line in lines:
if line.agent.is_group:
line.is_duet = False
continue
if line.agent.agent_id != main_agent:
main_agent = line.agent.agent_id
duet_status = not duet_status
line.is_duet = duet_status |
V2
main_agent = None
duet_status = False
for agent in agents:
if agent.is_person:
main_agent = agent.id
break
for line in lines:
if line.agent.is_group:
line.is_duet = True
continue
if line.agent.agent_id != main_agent:
main_agent = line.agent.agent_id
duet_status = not duet_status
line.is_duet = duet_status |
V3
main_agent = { person=None, group=None }
duet_status = { person=False, group=True }
for agent in agents:
if agent.is_person and main_agent.person is None:
main_agent.person = agent.id
elif agent.is_group and main_agent.group is None:
main_agent.group = agent.id
for line in lines:
if line.agent.is_group:
if line.agent.agent_id == main_agent.group:
main_agent.group = line.agent.agent_id
duet_status.group = not duet_status.group
line.is_duet = duet_status.group
else:
if line.agent.agent_id != main_agent.person:
main_agent.person = line.agent.agent_id
duet_status.person = not duet_status.person
line.is_duet = duet_status.person |
V4
first_agent = { person=None, group=None }
main_agent = None
duet_status = False
for agent in agents:
if agent.is_person and first_agent.person is None:
first_agent.person = agent.id
elif agent.is_group and first_agent.group is None:
first_agent.group = agent.id
main_agent = first_agent.person if first_agent.person is not None else (first_agent.group if first_agent.group is not None else agents[0].id)
for line in lines:
if line.agent.agent_id != main_agent:
main_agent = line.agent.agent_id
duet_status = not duet_status
line.is_duet = duet_statusNote 为什么这样写? 即便是 V3 版本,仍然会出现这样的情况:
因此引入了 V4 的处理逻辑。在我看来,听众并不会过于关心谁具体被分到了哪一边,真正需要的是听觉与视觉的统一:只要我听到演唱者切换了(无论是 person、group 还是 other),歌词就自动换到另一侧显示,与上一演唱者的行形成明确区分。 但是这种处理方式依然不够尽善尽美:在一些场景中(例如古典交响乐 男 person & 女 person & 合唱团 group),合唱往往是作为对唱以外的第三者出现,而 V4 这种逻辑会导致合唱也参与到对唱的分配中,一方面会让合唱左右跑,另一方面会导致男声和女生对唱中交换位置。 |
V5
person_s = []
other_s = []
normal_group_s = []
special_group_s = []
for agent in agents:
if agent.is_person:
person_s.append(agent.id)
elif agent.is_group:
normal_group_s.append(agent.id)
else:
other_s.append(agent.id)
for i in range(len(normal_group_s) - 1, -1, -1):
def find_agent_by_id(agent_id):
for agent in agents:
if agent.id == agent_id:
return agent
return None
group_agent = find_agent_by_id(normal_group_s[i])
special = len(person_s) != 0
for agent_id in group_agent.members:
if agent_id in person_s:
special = False
break
if special:
special_group_s.append(normal_group_s[i])
normal_group_s.pop(i)
if len(normal_group_s) == 1:
normal_group_agent = find_agent_by_id(normal_group_s[0])
if len(normal_group_agent.members) == len(person_s):
have_all_person = True
for agent_id in normal_group_agent.members:
if agent_id not in person_s:
have_all_person = False
break
if have_all_person:
special_group_s.append(normal_group_s[0])
normal_group_s.pop(0)
main_agent = { normal: person_s[0] if person_s else (normal_group_s[0] if normal_group_s else (other_s[0] if other_s else None)), special: special_group_s[0] if special_group_s else None }
duet_status = { normal: False, special: True } # 个人喜好,按照 AM 的逻辑可以将 special 设为 False
for line in lines:
if line.agent in special_group_s:
if line.agent != main_agent.special:
main_agent.special = line.agent
duet_status.special = not duet_status.special
line.is_duet = duet_status.special
else:
if line.agent != main_agent.normal:
main_agent.normal = line.agent
duet_status.normal = not duet_status.normal
line.is_duet = duet_status.normalNote Deepseek🐳 总结: 算法提升与问题总结提升相比前两套逻辑,本算法通过将演唱者分为 普通组(normal) 和 特殊组(special),并维护两套独立的状态机,同时解决了两种复杂场景:
可能出现的问题
综上,该算法在典型混合场景下表现良好,但需注意边缘分类与代码可读性。 |
V6
def process_duet(agents, lines):
"""
根据 agents 构成自动选择对唱检测逻辑,处理 lines,为每一行添加 is_duet 属性。
"""
# ========== 1. 识别阶段 ==========
person_ids = {agent.id for agent in agents if agent.is_person}
has_person = len(person_ids) > 0
# 检查是否存在“无关合唱团”:团体中有成员不在 person_ids 中
has_unrelated_group = False
for agent in agents:
if agent.is_group and hasattr(agent, 'members'):
# 如果团体成员不完全属于个人集合,则为无关合唱团
if not all(m in person_ids for m in agent.members):
has_unrelated_group = True
break
# ========== 2. 选择阶段 ==========
# 决策规则:
# - 若个人数量 < 2 → 无法构成对唱,直接全部标记 False
# - 若存在无关合唱团 → 使用 AppleMusic 风格(忽略所有团体)
# - 否则 → 使用平等对待风格(所有演唱者平等参与切换)
if len(person_ids) < 2:
# 边缘情况:少于两个个人,没有对唱意义
for line in lines:
line.is_duet = False
return
use_apple_style = has_unrelated_group # True: 忽略团体;False: 平等对待
# ========== 3. 处理阶段 ==========
if use_apple_style:
# ---------- AppleMusic 风格:忽略团体,仅个人之间切换 ----------
# 找到第一个个人作为主唱
main_agent = None
for agent in agents:
if agent.is_person:
main_agent = agent.id
break
# 如果意外没有个人(理论上前面已判断 len>=2),则全部标记 False
if main_agent is None:
for line in lines:
line.is_duet = False
return
duet_status = False
for line in lines:
# 团体行直接标记 False,不参与状态机
if line.agent.is_group:
line.is_duet = False
continue
# 个人行:切换检测
if line.agent.agent_id != main_agent:
main_agent = line.agent.agent_id
duet_status = not duet_status
line.is_duet = duet_status
else:
# ---------- 平等对待风格:所有演唱者(个人/团体)平等参与切换 ----------
# 初始主唱:优先取第一个个人,若没有则取第一个团体,再取第一个任意 agent
main_agent = None
for agent in agents:
if agent.is_person:
main_agent = agent.id
break
if main_agent is None:
for agent in agents:
main_agent = agent.id
break
if main_agent is None: # 无任何 agent
for line in lines:
line.is_duet = False
return
duet_status = False
for line in lines:
if line.agent.agent_id != main_agent:
main_agent = line.agent.agent_id
duet_status = not duet_status
line.is_duet = duet_status |
|
经过对 Android Apple Music 的进一步观察,补充一下几个关于解析和对唱布局的细节: 1. 演唱者目前观察到,Android Apple Music 能够解析以下五种演唱者类型:
Note 演唱者类型定义在 一个示例为 <head>
<metadata>
<ttm:agent type="person" xml:id="v1">
<ttm:name type="full">艺人A</ttm:name>
</ttm:agent>
</metadata>
</head>实际观察发现,歌词文件中 此外,Apple Music 还支持解析以下几种演唱者名称类型:
每个演唱者节点下仅允许指定一种名称类型,不支持同时声明全名与别名等多个名称字段 2. 对唱布局
效果与之前 Gemini 描述的异者翻转,同者保持策略一致。
根据观察,
和之前观察一致, |
Uh oh!
There was an error while loading. Please reload this page.
对话内容来自 @apoint123
Note
这是一份非常精彩且结构清晰的数据提取。通过追踪你提供的 10 个
ttm:agent(v1 到 v10)在时间轴上的左右分布,我们可以推导出 Apple Music 歌词对唱系统背后一套非常优雅的动态状态机逻辑。Apple Music 并没有把“左”或“右”写死在某一个歌手的 ID 上,而是根据“角色类型”和“上下文交替”来动态计算渲染位置。以下是具体的规律分析:
1.
type="group"(合唱)具有绝对的左侧锚定权在你的文件头部,
v4是唯一一个type="group"的角色。v4演唱段落(L10-11, L22-23, L27, L43, L54-55, L57),它们无一例外全部都在左侧,没有发生过任何翻转。2.
type="person"(独唱)遵循“异者翻转,同者保持”的动态交替这是这套机制中最核心的部分,也是实现“对话感”的关键。左右位置并不与
v1或v2永久绑定,而是实时计算出来的。规律: 默认起始位置为左侧(如开篇的
v1)。当出现一个与上一次演唱者不同的person时,系统会将其分配到与上一个person相反的一侧。如果紧接着还是同一个人,则停留在当前侧。证据: 我们可以看一段最密集的对唱部分(L49 到 L53):
L49 (v10): 左侧 (新歌手出场)
L50 (v1): 右侧 (由于 v1$\neq$ v10,发生翻转)
L51 (v3): 左侧 (由于 v3$\neq$ v1,发生翻转)
L52 (v1): 右侧 (由于 v1$\neq$ v3,发生翻转)
L53 (v1): 右侧 (由于 v1$=$ v1,不翻转,保持同侧)
这证明了系统是在动态对比“当前歌手”和“上一个歌手”的 ID。
3. 合唱 (
group) 会被交替状态机“无视”(穿透效应)这套算法最巧妙的设计在于,当多人在交替对唱时,如果中间插入了一句全员合唱(
group),合唱并不会打断或重置之前独唱歌手的左右状态。person应该在左还是在右时,会直接忽略中间的group标签,去寻找上一个真正发声的person进行比对。总结
Apple Music 的歌词排版引擎通过以下简单的规则实现了复杂的对唱视觉效果:
这个逻辑不仅减少了人工在 TTML 文件中硬编码排版样式的成本,还能确保无论多少个歌手参与,UI 上都能完美呈现出视觉上左右交织的对话感。
All reactions