commit bf8b2462da2550555f7fd190b080ec35c947d653 Author: yumoqing Date: Sat Jul 4 20:11:45 2026 +0800 feat: initial commit - songrate-service diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28aa085 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +*.pyc +*.pyo +logs/ +*.log +nohup.out +py3/ +*.egg-info/ +*.pid diff --git a/ah.py b/ah.py new file mode 100644 index 0000000..91b9cd9 --- /dev/null +++ b/ah.py @@ -0,0 +1,59 @@ +# -*- coding:utf-8 -*- +"""songrate standalone service - 音乐评估 GPU 服务 (端口 8900)""" +import json +import os +import sys +import tempfile +import asyncio + +from ahserver.webapp import webapp +from ahserver.serverenv import ServerEnv +from appPublic.registerfunction import RegisterFunction +from appPublic.log import debug + +# Add songrate package to path +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)))) + +from songrate.evaluator import evaluate_song + +async def do_evaluate(request, *args, **kw): + """POST /api/evaluate - 评估歌曲""" + env = request._run_ns + pkw = env.params_kw + + scene = getattr(pkw, 'scene', 'pop') or 'pop' + filepath = getattr(pkw, 'filepath', '') or '' + audio_path = getattr(pkw, 'audio_path', '') or '' + + if not filepath: + filepath = audio_path + + if not filepath: + return json.dumps({"error": "missing filepath or audio_path"}, ensure_ascii=False) + + # If it's a relative/web path, resolve to local + if not os.path.isabs(filepath): + from ahserver.filestorage import FileStorage + fs = FileStorage() + filepath = fs.realPath(filepath) + + if not os.path.exists(filepath): + return json.dumps({"error": f"file not found: {filepath}"}, ensure_ascii=False) + + # Run evaluation in thread pool (GPU bound) + from concurrent.futures import ThreadPoolExecutor + loop = asyncio.get_event_loop() + with ThreadPoolExecutor(max_workers=1) as pool: + result = await loop.run_in_executor(pool, evaluate_song, filepath, scene) + + return json.dumps(result, ensure_ascii=False) + + +def init(): + rf = RegisterFunction() + rf.register('evaluate', do_evaluate) + debug('songrate service initialized') + + +if __name__ == '__main__': + webapp(init) diff --git a/conf/config.json b/conf/config.json new file mode 100644 index 0000000..b9b5245 --- /dev/null +++ b/conf/config.json @@ -0,0 +1,15 @@ +{ + "website":{ + "paths":[["$[workdir]$/wwwroot",""]], + "client_max_size":500000000, + "host":"0.0.0.0", + "port":8900, + "coding":"utf-8", + "indexes":["index.html","index.dspy"], + "processors":[[".dspy","dspy"]], + "startswiths":[ + {"leading":"/api/evaluate","registerfunction":"evaluate"}, + {"leading":"/idfile","registerfunction":"idfile"} + ] + } +} diff --git a/songrate b/songrate new file mode 120000 index 0000000..5fadffd --- /dev/null +++ b/songrate @@ -0,0 +1 @@ +/data/ymq/songrate/songrate \ No newline at end of file diff --git a/songrate_audio_patch.py b/songrate_audio_patch.py new file mode 100644 index 0000000..804375f --- /dev/null +++ b/songrate_audio_patch.py @@ -0,0 +1,28 @@ +"""Patch songrate's load_audio to use soundfile instead of torchaudio""" +import soundfile as sf +import numpy as np +import torch + +def load_audio_patched(filepath, sr=22050): + """使用 soundfile 加载音频,避免 torchcodec 依赖""" + # 读取音频 + data, orig_sr = sf.read(filepath) + + # 转单声道 + if len(data.shape) > 1: + data = np.mean(data, axis=1) + + # 重采样 (使用 librosa) + if orig_sr != sr: + import librosa + data = librosa.resample(data, orig_sr=orig_sr, target_sr=sr) + + # 转 tensor 并移到 GPU + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + waveform = torch.from_numpy(data).float().to(device) + + return waveform, sr + +# Monkey patch +import songrate.analyzers +songrate.analyzers.load_audio = load_audio_patched