- hexi core: message handling, rate limiting, cooldown, plugin manager - Custom plugins: BF stats, daily check-in, quotes, persona cards, etc. - Community plugins vendored under hexi/plugins with local fixes - Web admin frontends (learning-chat, persona-admin), unified hexi/web - Tests for rate_limit/cooldown/memes/persona; poetry.lock Co-Authored-By: Claude <noreply@anthropic.com>
255 lines
7.4 KiB
Python
255 lines
7.4 KiB
Python
# -*- coding: UTF-8 -*-
|
||
#鬼畜音源的活字印刷
|
||
#作者:DSP_8192
|
||
|
||
import soundfile as sf
|
||
import psola
|
||
import numpy as np
|
||
from pypinyin import lazy_pinyin
|
||
import json
|
||
from pathlib import Path
|
||
from io import BytesIO
|
||
import base64
|
||
|
||
|
||
|
||
|
||
#--------------------------------------------
|
||
#全局变量
|
||
#--------------------------------------------
|
||
#目标采样率
|
||
_targetSR = 44100
|
||
|
||
|
||
|
||
|
||
#--------------------------------------------
|
||
#自定义函数
|
||
#--------------------------------------------
|
||
#文件路径转文件夹路径
|
||
def _fileName2FolderName(fileName):
|
||
for i in range(len(fileName)-1, -1, -1):
|
||
if fileName[i] == '\\' or fileName[i] == '/':
|
||
return fileName[0:i+1]
|
||
|
||
|
||
|
||
#标准化音频,统一音量
|
||
def _normalizeAudio(data):
|
||
rms = np.sqrt(np.mean(data**2))
|
||
normData = data / rms * 0.2
|
||
return normData
|
||
|
||
|
||
|
||
#读取音频文件
|
||
def _loadAudio(fileDir, norm):
|
||
data, sampleRate = sf.read(fileDir)
|
||
#双声道转单声道
|
||
if (len(data.shape) == 2):
|
||
#左右声道相加除以2
|
||
data = (data[:, 0] + data[:, 1]) / 2
|
||
#统一采样率
|
||
if (sampleRate != _targetSR):
|
||
#计算转换后的长度
|
||
newLength = int((_targetSR / sampleRate) * len(data))
|
||
#转换
|
||
data = np.interp(np.array(range(newLength)), np.linspace(0,newLength-1,len(data)), data)
|
||
#标准化
|
||
if norm:
|
||
data = _normalizeAudio(data)
|
||
return data
|
||
|
||
|
||
|
||
#改变音高和速度
|
||
def _modifyPitchAndSpeed(data, pitchMultiple, speedMultiple):
|
||
if (pitchMultiple == 1 and speedMultiple == 1):
|
||
#没有改动的必要,直接返回
|
||
return data
|
||
|
||
elif (pitchMultiple > 2 or speedMultiple < 0.5):
|
||
print("过于极端的音调和速度参数可能导致输出结果与预期不符,故不作改动")
|
||
return data
|
||
|
||
else:
|
||
#第一次拉伸
|
||
if (speedMultiple / pitchMultiple == 1):
|
||
#没有拉伸的必要
|
||
step1 = data
|
||
else:
|
||
#不改变音高的同时在时间上拉伸(PSOLA)
|
||
#constant_stretch过小会导致bug,因此分两次拉伸
|
||
step1 = psola.vocode(data, _targetSR, constant_stretch=1/pitchMultiple)
|
||
step1 = psola.vocode(step1, _targetSR, constant_stretch=speedMultiple)
|
||
#第二次拉伸,以改变音高的方式拉伸回来
|
||
newLength = int(len(data) / speedMultiple)
|
||
step2 = np.interp(np.array(range(newLength)), np.linspace(0,newLength-1,len(step1)), step1)
|
||
return step2
|
||
|
||
|
||
|
||
|
||
#--------------------------------------------
|
||
#活字印刷类
|
||
#--------------------------------------------
|
||
class huoZiYinShua:
|
||
def __init__(self, configuration):
|
||
try:
|
||
#读取设置文件
|
||
dictFile = open(configuration["dictFile"], encoding="utf8") #读取单字词典 (json)
|
||
ysddTableFile = open(configuration["ysddTableFile"], encoding="utf8") #读取原声大碟文本与文件名对照表 (json)
|
||
|
||
self.__voicePath = configuration["sourceDirectory"] #单字音频文件存放目录
|
||
self.__ysddPath = configuration["ysddSourceDirectory"] #原声大碟音频文件存放目录
|
||
self.__dictionary = json.load(dictFile) #定义非中文字符读法的词典
|
||
self.__ysddTable = json.load(ysddTableFile) #原声大碟文本与文件名对照表
|
||
|
||
#统一为小写
|
||
dictItems = list(self.__ysddTable.items())
|
||
#转换为list是为了切断dictItems与self.__ysddTable的联系,否则报错dictionary changed size during iteration
|
||
for i in dictItems:
|
||
self.__ysddTable[i[0].lower()] = self.__ysddTable.pop(i[0])
|
||
#从长到短排序,越长片段优先级越高
|
||
self.__ysddTable = sorted(self.__ysddTable.items(),
|
||
key=lambda x:len(x[0]),
|
||
reverse=True)
|
||
self.__ysddTable = dict(self.__ysddTable)
|
||
|
||
self.__configSucceed = True
|
||
|
||
except Exception as e:
|
||
self.__configSucceed = False
|
||
print(e)
|
||
|
||
|
||
|
||
#配置是否成功
|
||
def configSucceed(self):
|
||
return self.__configSucceed
|
||
|
||
|
||
|
||
|
||
#返回b64音频
|
||
def export(self, rawData, inYsddMode=False,
|
||
pitchMult=1, speedMult=1, norm=False, reverse=False):
|
||
self.__concatenate(rawData, inYsddMode, pitchMult, speedMult, norm, reverse)
|
||
base64_data = base64.b64encode(self.__export().getvalue())
|
||
return base64_data
|
||
|
||
|
||
|
||
#生成中间文件
|
||
def __concatenate(self, rawData, inYsddMode, pitchMult, speedMult, norm, reverse):
|
||
missingPinyin = []
|
||
self.__concatenated = np.array([])
|
||
|
||
#预处理,转为小写
|
||
rawData = rawData.lower()
|
||
pronunciations = []
|
||
|
||
#分割使用活字印刷的部分和使用原声大碟的部分
|
||
splitted = [[rawData, False]] #[文本, 是否使用原声大碟]
|
||
#遍历要匹配的句子
|
||
if inYsddMode:
|
||
for ysdd in self.__ysddTable.items():
|
||
#遍历文本
|
||
i = -1
|
||
while i < (len(splitted) - 1):
|
||
i += 1
|
||
if splitted[i][1]: #已经被划分为原声大碟部分
|
||
continue
|
||
#存在匹配
|
||
if ysdd[0] in splitted[i][0]:
|
||
indexBegin = splitted[i][0].index(ysdd[0]) #获取开始位置
|
||
#分割
|
||
splitted.insert(i+1, [splitted[i][0][indexBegin:indexBegin+len(ysdd[0])], True])
|
||
splitted.insert(i+2, [splitted[i][0][indexBegin+len(ysdd[0]):], False])
|
||
splitted[i][0] = splitted[i][0][0:indexBegin]
|
||
|
||
|
||
#转换自定义的字符
|
||
for i in range(0, len(splitted)):
|
||
pronunciations.append([])
|
||
#使用活字印刷
|
||
if (not splitted[i][1]):
|
||
pronunciations[i].append("")
|
||
for ch in splitted[i][0]:
|
||
if ch in self.__dictionary:
|
||
#词典中存在匹配,转换
|
||
pronunciations[i][0] += self.__dictionary[ch] + " "
|
||
else:
|
||
#保持不变
|
||
pronunciations[i][0] += ch + " "
|
||
pronunciations[i].append(False) #标记
|
||
#使用原声大碟
|
||
else:
|
||
pronunciations[i].append(splitted[i][0]) #直接复制
|
||
pronunciations[i].append(True) #标记
|
||
|
||
|
||
#拼接音频
|
||
for i in range(0, len(pronunciations)):
|
||
#使用活字印刷
|
||
if (not pronunciations[i][1]):
|
||
#将汉字转换成拼音
|
||
pinyin = lazy_pinyin(pronunciations[i][0])
|
||
#拆成单独的字
|
||
for text in pinyin:
|
||
for word in text.split():
|
||
#拼接每一个字
|
||
try:
|
||
self.__concatenated = np.concatenate((self.__concatenated,
|
||
_loadAudio(self.__voicePath
|
||
+ word + ".wav",
|
||
norm)))
|
||
#如果出现错误
|
||
except Exception as e:
|
||
if word != ",":
|
||
print(e) #显示错误信息
|
||
#加入缺失素材列表
|
||
if word not in missingPinyin:
|
||
missingPinyin.append(word)
|
||
#以空白音频代替
|
||
self.__concatenated = np.concatenate((self.__concatenated,
|
||
np.zeros(int(_targetSR/4))))
|
||
|
||
#使用原声大碟
|
||
else:
|
||
#拼接
|
||
try:
|
||
self.__concatenated = np.concatenate((self.__concatenated,
|
||
_loadAudio(self.__ysddPath
|
||
+ self.__ysddTable[pronunciations[i][0]],
|
||
norm)))
|
||
#如果出现错误
|
||
except Exception as e:
|
||
print(e) #显示错误信息
|
||
#加入缺失素材列表
|
||
if self.__ysddTable[pronunciations[i][0]] not in missingPinyin:
|
||
missingPinyin.append(self.__ysddTable[pronunciations[i][0]])
|
||
#以空白音频代替
|
||
self.__concatenated = np.concatenate((self.__concatenated,
|
||
np.zeros(int(_targetSR/4))))
|
||
|
||
|
||
#音高偏移
|
||
self.__concatenated = _modifyPitchAndSpeed(self.__concatenated, pitchMult, speedMult)
|
||
|
||
#倒放
|
||
if(reverse):
|
||
self.__concatenated = np.flip(self.__concatenated)
|
||
|
||
#如果缺失拼音,则发出警告
|
||
#if len(missingPinyin) != 0:
|
||
# print("警告:缺失或未定义{}".format(missingPinyin))
|
||
|
||
|
||
|
||
#返回二进制音频
|
||
def __export(self):
|
||
bytesout = BytesIO()
|
||
sf.write(bytesout, self.__concatenated, _targetSR, format="MP3")
|
||
return bytesout
|