Compare commits

..

6 Commits

Author SHA1 Message Date
bd69ee2b9c 修复了比例问题 2025-06-07 22:58:57 +08:00
0e029febdd 宴会 2025-06-06 17:13:51 +08:00
e73568a1fe PRiSM 2025-05-12 20:32:39 +08:00
e85f7e86ee PRiSM 2025-05-11 10:52:18 +08:00
5a082b63a4 优化单位数ID 2025-05-10 23:16:18 +08:00
8ef9d502f1 优化单位数ID 2025-05-10 23:10:52 +08:00
5 changed files with 125 additions and 26 deletions

View File

@@ -20,6 +20,9 @@ def parse_music_xml(file_path):
artist_elem = root.find("artistName/str") artist_elem = root.find("artistName/str")
artist_name = artist_elem.text if artist_elem is not None else "?" artist_name = artist_elem.text if artist_elem is not None else "?"
version_elem = root.find("AddVersion/str")
version_name = version_elem.text if version_elem is not None else "?"
# 提取启用谱面信息(设计者 + 等级) # 提取启用谱面信息(设计者 + 等级)
note_infos = [] note_infos = []
notes_data = root.find("notesData") notes_data = root.find("notesData")
@@ -45,12 +48,12 @@ def parse_music_xml(file_path):
"levelshow": f"{level}{PLUS if int(level_decimal) > 6 else EMPTY}", "levelshow": f"{level}{PLUS if int(level_decimal) > 6 else EMPTY}",
}) })
return id,music_name, artist_name, note_infos return id,music_name, artist_name, note_infos , version_name
if __name__ == "__main__": if __name__ == "__main__":
file_path = "/Users/bennett/Downloads/SDEZ/Package/Sinmai_Data/StreamingAssets/A000/music/music011663/Music.xml" file_path = "/Users/bennett/Downloads/SDEZ/Package/Sinmai_Data/StreamingAssets/A000/music/music011663/Music.xml"
id,music_name, artist_name, note_infos = parse_music_xml(file_path) id,music_name, artist_name, note_infos,version_name = parse_music_xml(file_path)
print("ID:",id) print("ID:",id)
print("音乐名称:", music_name) print("音乐名称:", music_name)
print("艺术家名称:", artist_name) print("艺术家名称:", artist_name)

View File

@@ -8,7 +8,7 @@ from acb2mp3 import convert_awb_to_wav, convert_wav_to_mp3
from search import search_music_by_id from search import search_music_by_id
from pv_decode import dat_to_mp4 from pv_decode import dat_to_mp4
from loguru import logger from loguru import logger
from concurrent.futures import ThreadPoolExecutor, as_completed
# 假设你已实现以下函数 # 假设你已实现以下函数
def build_maidata_txt( def build_maidata_txt(
@@ -71,6 +71,9 @@ def convert_to_simai_folder(result,output_folder):
acb_list = result[3] acb_list = result[3]
awb_file = next((f for f in acb_list if f.endswith('.awb')), None) awb_file = next((f for f in acb_list if f.endswith('.awb')), None)
dat_file = result[4] dat_file = result[4]
versionname = result[5]
if name is None or artist is None or designers is [] or levels is [] or ma2_list is [] or acb_list is [] or awb_file is None or dat_file is None or versionname is None:
return None
convert_results = {} convert_results = {}
for path in ma2_list: for path in ma2_list:
filename = os.path.basename(path) filename = os.path.basename(path)
@@ -93,7 +96,7 @@ def convert_to_simai_folder(result,output_folder):
source_folder = of source_folder = of
# 目标文件夹路径 # 目标文件夹路径
target_folder = f"{npof}/{name}" target_folder = f"{npof}/{versionname}/{name}"
# 要复制的文件列表 # 要复制的文件列表
files_to_copy = ["bg.png", "maidata.txt", "pv.mp4", "track.mp3"] files_to_copy = ["bg.png", "maidata.txt", "pv.mp4", "track.mp3"]
@@ -133,8 +136,28 @@ def convert_to_simai_folder(result,output_folder):
# 示例调用 # 示例调用
if __name__ == "__main__": if __name__ == "__main__":
res = search_music_by_id(input("ID:")) music_ids = [834,799]
logger.info(res) output_folder = "result"
logger.info("Converting...") max_workers = 6 # 根据 CPU 和硬盘负载合理设置线程数
convert_to_simai_folder(res,"result")
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {}
for mid in music_ids:
res = search_music_by_id(str(mid))
if res is None:
continue
logger.info(f"提交任务: {mid}")
future = executor.submit(convert_to_simai_folder, res, output_folder)
futures[future] = mid
for future in as_completed(futures):
mid = futures[future]
try:
result = future.result()
logger.info(f"{mid} 处理完成")
except Exception as e:
logger.error(f"{mid} 处理出错: {e}")

View File

@@ -1,6 +1,24 @@
import os
import subprocess import subprocess
import sys import sys
from fractions import Fraction
from loguru import logger
def ratio_to_str(width, height, max_denominator=20):
"""将宽高比转换为 a:b 的形式"""
# 确保分子 > 分母
if width >= height:
ratio = Fraction(width, height).limit_denominator(max_denominator)
else:
ratio = Fraction(height, width).limit_denominator(max_denominator)
a, b = ratio.numerator, ratio.denominator
# 由于上面取了 max(width, height),要对应宽:高
if width >= height:
return f"{a}:{b}"
else:
return f"{b}:{a}"
def get_video_resolution(video_path): def get_video_resolution(video_path):
"""获取视频的宽高信息""" """获取视频的宽高信息"""
@@ -13,7 +31,15 @@ def get_video_resolution(video_path):
return width, height return width, height
def calculate_padding(width, height): def calculate_padding(width, height):
"""计算填充黑边,使其变成 1:1正方形""" """计算填充黑边,使其变成 1:1正方形
如果接近4:3或1:1比例90%以内则直接返回0。
"""
aspect_ratio = width / height if width >= height else height / width
# 如果接近 4:3 或 1:1就不填充
if (0.9 <= aspect_ratio <= 1.1) or (1.3 <= aspect_ratio <= 1.5):
return max(width, height), 0, 0
max_side = max(width, height) # 以最大边长作为正方形边长 max_side = max(width, height) # 以最大边长作为正方形边长
pad_x = (max_side - width) // 2 pad_x = (max_side - width) // 2
pad_y = (max_side - height) // 2 pad_y = (max_side - height) // 2
@@ -25,7 +51,7 @@ def process_video(input_file, output_file):
target_size, pad_x, pad_y = calculate_padding(width, height) target_size, pad_x, pad_y = calculate_padding(width, height)
if pad_x == 0 and pad_y == 0: if pad_x == 0 and pad_y == 0:
print("视频已经是 1:1,无需填充。") logger.info(f"视频比例是 {ratio_to_str(width,height)},无需填充。")
ffmpeg_cmd = [ ffmpeg_cmd = [
"ffmpeg", "-i", input_file, "ffmpeg", "-i", input_file,
"-c:v", "h264_videotoolbox", "-c:v", "h264_videotoolbox",
@@ -34,7 +60,7 @@ def process_video(input_file, output_file):
output_file output_file
] ]
else: else:
print(f"填充黑边,使视频变为 {target_size}x{target_size}") logger.info(f"视频比例是 {ratio_to_str(width,height)}填充黑边,使视频变为 {target_size}x{target_size}")
ffmpeg_cmd = [ ffmpeg_cmd = [
"ffmpeg", "-i", input_file, "ffmpeg", "-i", input_file,
"-vf", f"pad={target_size}:{target_size}:{pad_x}:{pad_y}:black", "-vf", f"pad={target_size}:{target_size}:{pad_x}:{pad_y}:black",
@@ -48,7 +74,7 @@ def process_video(input_file, output_file):
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) < 3: if len(sys.argv) < 3:
print("用法: python pv_convert.py 输入文件 输出文件") logger.info("用法: python pv_convert.py 输入文件 输出文件")
sys.exit(1) sys.exit(1)
input_file = sys.argv[1] input_file = sys.argv[1]

View File

@@ -38,26 +38,30 @@ def get_video_duration(path: Path) -> float:
return 0.0 return 0.0
def dat_to_mp4(dat_file: str, id: str): def dat_to_mp4(dat_file: str, id: str):
""" 将 .dat 文件当作 .usm 文件处理,提取并转换为 .mp4 """ """将 .dat 文件当作 .usm 文件处理,提取并转换为 .mp4"""
dat_path = Path(dat_file).resolve() dat_path = Path(dat_file).resolve()
base_name = dat_path.stem base_name = dat_path.stem
work_dir = Path("/Users/bennett/PJCK/CookiesChartConverter") / "work" / id work_dir = Path("/Users/bennett/PJCK/CookiesChartConverter") / "work" / id
usm_path = Path(dat_file) usm_path = dat_path
ivf_path = work_dir / "output" / f"{base_name}.dat" / "videos" / f"{base_name}.ivf" ivf_dir = work_dir / "output" / f"{base_name}.dat" / "videos"
mp4_path = work_dir / f"{base_name}.mp4" mp4_path = work_dir / f"{base_name}.mp4"
# 直接将 .dat 当作 .usm 文件处理 # Step 1: 提取 USM 内容
print(f"[1/3] 提取 USM 内容 ...") print(f"[1/3] 提取 USM 内容 ...")
extract_usm(usm_path, work_dir) extract_usm(usm_path, work_dir)
if not ivf_path.exists(): # Step 2: 找到第一个 .ivf 文件
print(f"❌ 提取失败,未找到 {ivf_path.name}") ivf_files = list(ivf_dir.glob("*.ivf"))
return if not ivf_files:
print(f"❌ 提取失败,未找到 .ivf 文件")
return None
ivf_path = ivf_files[0]
print(f"[2/3] 转换为 MP4 ...") print(f"[2/3] 转换为 MP4 ...")
convert_ivf_to_mp4(ivf_path, mp4_path) convert_ivf_to_mp4(ivf_path, mp4_path)
# Step 3: 检查视频时长
duration = get_video_duration(mp4_path) duration = get_video_duration(mp4_path)
if duration < 1.0: if duration < 1.0:
print(f"⚠️ 视频时长 {duration:.2f}s 太短,跳过生成 pv.mp4") print(f"⚠️ 视频时长 {duration:.2f}s 太短,跳过生成 pv.mp4")
@@ -67,7 +71,6 @@ def dat_to_mp4(dat_file: str, id: str):
process_video(mp4_path, work_dir / "pv.mp4") process_video(mp4_path, work_dir / "pv.mp4")
return mp4_path return mp4_path
# === 示例用法 === # === 示例用法 ===
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys

View File

@@ -22,7 +22,7 @@ def search_music_by_id(search_id):
if not os.path.isfile(music_xml_path): if not os.path.isfile(music_xml_path):
continue continue
music_id, name, artist, notes = parse_music_xml(music_xml_path) music_id, name, artist, notes,version_name = parse_music_xml(music_xml_path)
if music_id == search_id: if music_id == search_id:
logger.info(f"\n【找到曲目:{name}") logger.info(f"\n【找到曲目:{name}")
logger.info(f" ID{music_id}") logger.info(f" ID{music_id}")
@@ -46,7 +46,13 @@ def search_music_by_id(search_id):
# 曲绘(查 jacket 文件夹) # 曲绘(查 jacket 文件夹)
jacket_dir = os.path.join(rd, "AssetBundleImages", "jacket") jacket_dir = os.path.join(rd, "AssetBundleImages", "jacket")
jacket_ab = f"ui_jacket_00{int(music_id)-10000}.ab" music_num = int(music_id)
if music_num >= 100000:
jacket_ab = f"ui_jacket_{(music_num-100000) - 10000:06d}.ab"
elif music_num >= 10000:
jacket_ab = f"ui_jacket_{music_num - 10000:06d}.ab"
else:
jacket_ab = f"ui_jacket_{music_num:06d}.ab"
alt_exts = [".png", ".jpg", ".jpeg"] alt_exts = [".png", ".jpg", ".jpeg"]
alt_jacket = next((f for f in os.listdir(jacket_dir) alt_jacket = next((f for f in os.listdir(jacket_dir)
if f.startswith(f"ui_jacket_{music_id}") if f.startswith(f"ui_jacket_{music_id}")
@@ -63,7 +69,14 @@ def search_music_by_id(search_id):
# 音频文件SoundData # 音频文件SoundData
sound_dir = os.path.join(rd, "SoundData") sound_dir = os.path.join(rd, "SoundData")
audio_prefix = f"music00{int(music_id)-10000}" music_num = int(music_id)
if music_num >= 100000:
audio_prefix = f"music{(music_num-100000) - 10000:06d}"
elif music_num >= 10000:
audio_prefix = f"music{music_num - 10000:06d}"
else:
audio_prefix = f"music{music_num:06d}"
audio_files = [f for f in os.listdir(sound_dir) if f.lower().startswith(audio_prefix)] audio_files = [f for f in os.listdir(sound_dir) if f.lower().startswith(audio_prefix)]
logger.info(" 音频文件:") logger.info(" 音频文件:")
if audio_files: if audio_files:
@@ -78,17 +91,48 @@ def search_music_by_id(search_id):
# 视频 dat 文件MovieData # 视频 dat 文件MovieData
movie_dir = os.path.join(rd, "MovieData") movie_dir = os.path.join(rd, "MovieData")
dat_name = f"00{int(music_id)-10000}.dat" music_num = int(music_id)
if music_num >= 100000:
dat_name = f"{(music_num-100000) - 10000:06d}.dat"
elif music_num >= 10000:
dat_name = f"{music_num - 10000:06d}.dat"
else:
dat_name = f"{music_num:06d}.dat"
dat_path = os.path.join(movie_dir, dat_name) dat_path = os.path.join(movie_dir, dat_name)
logger.info(" 视频 DAT 文件:") logger.info(" 视频 DAT 文件:")
logger.info(f" - {dat_path}" if os.path.exists(dat_path) else " - 未找到") logger.info(f" - {dat_path}" if os.path.exists(dat_path) else " - 未找到")
return [[music_id, name, artist,notes],ma2_paths,os.path.join(jacket_dir, jacket_ab),audio_lists,dat_path] return [[music_id, name, artist,notes],ma2_paths,os.path.join(jacket_dir, jacket_ab),audio_lists,dat_path,version_name]
logger.error(f"\n未找到 ID 为 {search_id} 的曲目信息。") logger.error(f"\n未找到 ID 为 {search_id} 的曲目信息。")
return None return None
def list_all_music():
music_list = []
for asset_dir in os.listdir(streaming_assets):
root_dir = os.path.join(streaming_assets, asset_dir)
music_dir = os.path.join(root_dir, "music")
if not os.path.isdir(music_dir):
continue
for music_subdir in os.listdir(music_dir):
sub_path = os.path.join(music_dir, music_subdir)
music_xml_path = os.path.join(sub_path, "Music.xml")
if not os.path.isfile(music_xml_path):
continue
music_id, name, artist, notes, version_name = parse_music_xml(music_xml_path)
music_list.append({
"id": music_id,
"name": name,
"artist": artist,
"version": version_name
})
return music_list
if __name__ == "__main__": if __name__ == "__main__":