Kaldi学习之如何运行脚本

学习Kaldi也有两三个星期了,基本上就是对Kaldi官网上的说明简单摸索了下,顺便跑了跑自带的例子。Kaldi的官网真是个大百科,内容非常详细,在此也没办法全部看完,只能慢慢将最基础的部分写出来(包括对原文的翻译以及一点自己的理解)。关于数据准备和语音解码更详细的信息会在后续的文章写到。

以下部分源自Kaldi脚本运行的说明:http://kaldi-asr.org/doc/tutorial_running.html

如何安装Kaldi环境
1、安装Kaldi依赖,以Centos为例
依赖库安装
yum install libtool
yum install autoconf
yum install wget
yum install perl
yum install subversion
yum install zlib
yum groupinstall “Development Tools”
2、下载Kaldi源码(Kaldi暂时只支持Linux环境)

git clone https://github.com/kaldi-asr/kaldi.git kaldi --origin upstream

3、在Kaldi的顶层目录中找到README.txt和INSTALL文件,按照说明依次找下级目录的README.txt和INSTALL文件进行安装即可。
具体的Kaldi官方版本INSTALL说明
一、找到Kaldi源目录INSTALL,内容:
(1)
go to tools/ and follow INSTALL instructions there.
(2)
go to src/ and follow INSTALL instructions there.
二、进入tools目录INSTALL,执行:
(1)
extras/check_dependencies.sh #检查依赖库安装情况
(2)
CXX=g++-4.8 extras/check_dependencies.sh #检查C++编译环境,如果不支持,设置CXX环境变量检查另一个编译器
(3)
make #单核
make -j n #n小于等于cpu核心数
这步之后默认会安装 ATLAS头文件, OpenFst, SCTK and sph2pipe。
注意OpenFst的环境要求,需要C++11支持。比如g++ >= 4.7, Apple clang >= 5.0 or LLVM clang >= 3.3,需要时候可以指定编译器版本
三、进入src目录,查看INSTALL
./configure –shared
make depend
make

数据准备
Oracle GridEngine是业界部署最广泛的负载管理解决方案,它提供了无与伦比的可伸缩性。Oracle Grid Engine 提供了一组丰富的高级调度功能,能够灵活适应任何计算环境和应用负载,从而为云计算模型提供全面支持。
本地小数据集(使用CPUs,不使用GPUs时一般在cmd.sh中配置如下)

train_cmd="run.pl"
decode_cmd="run.pl"

GridEngine

train_cmd="queue.pl -q all.q@a*.clsp.jhu.edu"
decode_cmd="queue.pl -q all.q@[ah]*.clsp.jhu.edu"

有一个例子是从RM数据集创建训练集和测试集(/export/corpora5/LDC/LDC93S3A/rm_comp是数据集路径)
local/rm_data_prep.sh需要自己编写,运行命令

local/rm_data_prep.sh /export/corpora5/LDC/LDC93S3A/rm_comp 

数据准备成功显示:RM_data_prep succeeded

脚本运行前当前目录结构(data是新生成的目录)

local : 包含当前数据的目录
•   train : 数据库中分离出来的训练数据.
•   test_* : 数据库中分离出来的测试数据.

数据准备的更多细节:Kaldi学习之数据准备详细解释说明

观察example中这些目录,假设当前所在目录是data目录
执行命令行:

cd local/dict
head lexicon.txt
head nonsilence_phones.txt
head silence_phones.txt

从这能够看到生成的一些准备数据信息
有些文件不是通过Kaldi得到的,而是通过OpenFst自己准备得到,比如

lexicon.txt : 词典文件
•silence.txt : 包含哪个音素是静音的,哪个音素不是静音的信息

到/train目录,查看生成文件信息(如果要用自己的数据集,以下文件必须自己手动处理生成

head text
head spk2gender.map #这个文件是说话人性别映射文件,很多情况用不上
head spk2utt
head utt2spk
head wav.scp

对上述文件的解释如下:

•   text – 此文件包含了语音和Kaldi要用的语音id的映射关系。这个文件会被转成整型格式-文本文件,但是文件名这些会被整型的id代替。
•   spk2gender.map – 文件包含说话人和他们的性别信息映射。这在训练的时候也表现了说话人的唯一性。
•   spk2utt – 这个是说话人标识和说话人对应所有语音标识的映射。
•   utt2spk – 这个是语音id和语音所属说话人的一对一的映射关系。
•   wav.scp – 特征提取的时候Kaldi会读取这个文件。它被解析成键值对,key是每行的第一个字符串(在例子里是语音的id,Integer型),值就是扩展的文件名。扩展文件名分为rxfilename(用于读)和wxfilename(用于写)。这两种文件名有各自的规则。
比如rxfilename:
"-"或者""意味着标准输入;
"some command |"意味着输入管道命令;
"/some/filename:12345"意味着文件的偏移量;
"/some/filename"…类似这样,任何不匹配前3条命令的都被当做普通文件来处理。
wxfilename也和rxfilename类似,除了没有文件偏移量。
具体可看http://www.kaldi-asr.org/doc/io.html#io_sec_xfilename。

注意Kaldi的.scp和HTK的.scp脚本文件是不一样的,后者只被当成普通文件处理。

验证训练集比测试集大很多的命令:

wc train/text test_feb89/text

下面的步骤就是用Kaldi去创建raw类型的语言模型
回到s5目录,执行以下命令:

utils/prepare_lang.sh data/local/dict '!SIL' data/local/lang data/lang

这就会创建lang文件夹,这里面包含了FST的描述所涉及的语言。prepare_lang.sh这个脚本把/data里面创建的一些文件转换成可以被Kaldi阅读的更规范的形式。这个脚本创建的输出文件在data/lang中。
接下来说的就是这里面的一些文件。

首先创建的两个文件就是words.txt和phones.txt,这是OpenFst格式符号表,是字符串到integer类型的映射关系。理解这种格式的含义:
http://www.kaldi-asr.org/doc/tutorial_looking.html
而理解kaldi更入门的需要理解openFst,链接:
http://www.openfst.org/twiki/bin/view/FST/WebHome
有限状态机分为:接收器/识别器(输出一个二元状态,是或者不是);变换器(使用动作基于给定的输入和/或状态生成输出)分为Moore和Mealy FSM。两种变换器定义:Moore状态机的输出只与当前的状态有关,即:输出=f(当前状态);Mealy状态机的输出与当前状态和输入有关,即:输出=f(当前状态,输入)。不管是Moore机还是Mealy机,两者的下一状态都与当前状态和输入有关,即:下一状态=f(当前状态,输入),这是两种状态机模型的共性。


回到kaldi的fst格式,首先创建一个状态转移矩阵

# arc format: src dest ilabel olabel [weight]
# final state format: state [weight]
# lines may occur in any order except initial state must be first line
# unspecified weights default to 0.0 (for the library-default Weight type)
cat >text.fst <<EOF
0 1 a x .5
0 1 b y 1.5
1 2 c z 2.5
2 3.5
EOF

创建输入和输出的结点以及对应的id

cat >isyms.txt <<EOF
<eps> 0
a 1
b 2
c 3
EOF

cat >osyms.txt <<EOF
<eps> 0
x 1
y 2
z 3
EOF

加入当前路径到环境变量

export PATH=.:$PATH

编译并创建二进制的FST

fstcompile --isymbols=isyms.txt --osymbols=osyms.txt text.fst binary.fst

把binary.fst的转移矩阵复制一份,权重乘以2倍生成到binary2.fst中

fstinvert binary.fst | fstcompose - binary.fst > binary2.fst

以文本形式查看fst转移矩阵

fstprint --isymbols=isyms.txt --osymbols=osyms.txt binary.fst
fstprint --isymbols=isyms.txt --osymbols=osyms.txt binary2.fst

删除生成的.fst和.txt信息

rm *.fst *.txt

看Kaldi的例子中
查看fst中的信息

fstprint --isymbols=data/lang/phones.txt --osymbols=data/lang/words.txt data/lang/L.fst | head

如果bash找不到fstprint命令,就要把OpenFST的安装路径加到系统环境变量中,简单跑下path.sh就行

. ./path.sh

下一步就是使用之前步骤创建的一些文件来生成FST描述的语言语法,回到s5目录执行

local/rm_prepare_grammar.sh

如果成功了,G.fst会在/data/lang中创建(PS:G代表Grammer,在识别中运用)

特征提取
mfcc特征提取
找到run.sh中与mfcc相关的三行(决定特征存放目录,编辑对应例子),比如把特征存放到/my/disk/rm_mfccdir,就执行对应的脚本

export featdir=/my/disk/rm_mfccdir #存放位置
# make sure featdir exists and is somewhere you can write.
# can be local if you want.
mkdir $featdir #创建目录
for x in test_mar87 test_oct87 test_feb89 test_oct89 test_feb91 test_sep92 train; do \
  steps/make_mfcc.sh --nj 8 --cmd "run.pl" data/$x exp/make_mfcc/$x $featdir; \ #生成mfcc
  steps/compute_cmvn_stats.sh data/$x exp/make_mfcc/$x $featdir; \ #计算cmvn状态,做均值和方差规整
done

PS:run.pl会在命令运行时生成一些log信息之类的,可运用到每条命令中
多个CPU并行跑,可以更改—nj选项决定多少个任务一起跑。
可以查看log文件exp/make_mfcc/train/make_mfcc.1.log看创建MFCCs的输出。

查看make_mfcc.sh中split_scp.pl作用(生成raw_mfcc_train.1.scp)
执行命令

wc $featdir/raw_mfcc_train.1.scp 
wc data/train/wav.scp

yesno数据集会得到如下效果:

[xx@localhost mfcc]$ wc raw_mfcc_train_yesno.1.scp
  31   62 2897 raw_mfcc_train_yesno.1.scp
[xx@localhost mfcc]$ wc ../data/train_yesno/wav.scp
  31   62 1488 ../data/train_yesno/wav.scp

Wc命令作用:统计字节数、字数、行数
如上则表示 31行 62个字符数(空格隔开的) 1488字节
wav.scp 表示 语音id 和 对应的 实际语音信息(存放目录)
raw_mfcc_train_yesno.1.scp 存放的是语音id 和 对应的总特征文件.ark中语音对应的字节偏移

查看调用计算mfcc特征(compute-mfcc-feats)脚本
调用这个工具时有个配置文件的机制,kaldi可以用这个文件来配置选项,就像HTK的配置文件一样,但是很少用。目标变量(scp和ark,scp这种需要更多解释)
在解释前,执行以下命令

head data/train/wav.scp
head $featdir/raw_mfcc_train.1.scp
less $featdir/raw_mfcc_train.1.ark

查看这些文件结构,ark文件是二进制文件

以文本形式看.ark文件的头几行

copy-feats ark:$featdir/raw_mfcc_train.1.ark ark,t:- | head

也可以去掉t:-(这代表text格式查看) 但是把管道置换为less更好,因为ark是二进制文件
还可以通过以下命令达到同样的效果

copy-feats scp:$featdir/raw_mfcc_train.1.scp ark,t:- | head

这是因为archive和script文件代表同样的东西(archive是script的1/8,分成了8份,注意ark:和scp:命令)。Kaldi不会从数据本身中找出什么是脚本文件或ark文件,实际上,Kaldi从不尝试从后缀区分文件。这是出于一般的哲学原因,也为了防止与管道的不良交互(因为管道通常没有名称)。

执行以下命令:

head -10 $featdir/raw_mfcc_train.1.scp | tail -1 | copy-feats scp:- ark,t:- | head

这条命令从1/10训练文件打印出一些数据。”scp:-“中的”-“表示标准输入读取,scp表示输入是脚本文件。

看script和archive文件到底是什么,第一点要做的就是用同样的方式看他们的代码。
举个简单的用户级别调用代码的例子,输入以下命令:

tail -30 ../../../src/featbin/copy-feats.cc

这里核心代码就三行。代码中迭代的方式和OpenFst中的状态迭代一样的(尝试和OpenFst中的尽可能样式一致)

其实script和archive文件就是一个“表”的概念。这个表就是一个排序好的条目(比如特征文件)集合,通过唯一的字符串(比如语音id)进行索引。“表”不是真的C++对象,因为C++对象访问数据是依靠是否写、迭代或者随机访问。这些对象类型的一个例子就是浮点矩阵(Matrix

BaseFloatMatrixWriter
RandomAccessBaseFloatMatrixReader
SequentialBaseFloatMatrixReader

这些实际上是模板类。

.scp和.ark文件都可以看成是数据表。这种格式有如下特点:

•   .scp格式是纯文本格式,一行有key的id和“可扩展文件名”让Kaldi去找数据
•   .ark格式可能是文本/二进制,”t”参数表示文本,默认是二进制。格式:key的id,空格,目标数据。

.scp和.ark文件几个通用的点:

•   指定读表的字符串叫rspecifier;比如 "ark:gunzip -c my/dir/foo.ark.gz|".
•   指定写表的字符串叫 wspecifier;比如 "ark,t:foo.ark"..ark文件可以共同连接起来,仍然是有效的ark文件(没有中心索引)
•   代码可以顺序或随机访问.scp.ark文件。用户级代码只需要知道它是迭代还是查找,不需要知道访问的是哪种类型文件。
•   Kaldi不会在.ark文件中表示对象类型;需要提前知道对象类型。
•   .ark.scp文件不包含混合类型
•   通过随机访问来读取.ark文件可能是无效的,因为代码可能必须将对象缓存在内存中。
•   为了有效地随机访问.ark文件,您可以使用“ark,scp”写入机制(例如用于将mfcc功能写入磁盘)来写出相应的脚本文件。 然后,通过scp文件访问它。
•   在档案上进行随机访问时,避免代码必须缓存一堆内容的另一种方法是告知代码归档归档并按排序顺序调用(例如“ark,s,cs: - ”))

更多信息看 Kaldi I/O mechanisms.
.scp和.ark怎么在管道内使用的命令:

head -1 $featdir/raw_mfcc_train.1.scp | copy-feats scp:- ark:- | copy-feats ark:- ark,t:- | head

注意输出要用head,否则会出现一大堆内容(.ark可能是二进制文件)

最后,为了方便起见,将所有测试数据合并到一个目录中。对这个一般的步骤进行所有的测试。 以下命令还将合并说话人,注意重复和重新生成这些说话人的统计信息,以便我们的工具不会出错。 通过运行以下命令(从s5目录)执行此操作。

utils/combine_data.sh data/test data/test_{mar87,oct87,feb89,oct89,feb91,sep92}
steps/compute_cmvn_stats.sh data/test exp/make_mfcc/test $featdir

创建训练数据的子集(train.1k),每个说话人只有1000条语音。
用此数据进行训练,命令:

utils/subset_data_dir.sh data/train 1000 data/train.1k 

单音子训练
训练单音子模型。如果安装Kaldi的磁盘不大,使用exp/作为到大容量磁盘的软链接(如果跑实验不清理数据,很容易到几个G),输入:

nohup steps/train_mono.sh --nj 4 --cmd "$train_cmd" data/train.1k data/lang exp/mono &

查看最近的输出命令:

tail nohup.out

可以用–nj这种方式跑更长时间的任务,这样在断开连接时候也能跑完数据。用”screen”方式会更好,它不会被kill掉。实际上这个脚本(train_mono.sh)的标准输出(命令行输出)和错误(ERROR)很少输出,大部分的输出都是输出到exp/mono/下的log文件。

运行的时候看文件data/lang/topo。文件是实时生成的。每一个音素都有从其它音素来的不同拓扑结构。看data/phones.txt从数字id可以看出它到底是哪个音素。拓扑文件中的约定是第一个状态是初始状态(概率为1),最后一个状态是最终状态(概率为1)。

查看模型文件命令:

gmm-copy --binary=false exp/mono/0.mdl - | less

可以看到拓扑文件头部信息,在模型参数之前,还有一些其它东西。.mdl文件包含两个对象:转移概率模型包括HMM拓扑信息,还有一种相关模型类的对象(AmGmm)。 通过“包含两个对象”,意思是对象具有标准形式的写入和读取功能,调用这些函数将对象写入文件。 对于这样的对象,这不是表的一部分(即没有涉及到“ark:”或“scp:”),写入二进制或文本模式,可以由标准的命令行选项设置 -binary = true或-binary = false(不同的程序具有不同的默认值)。 对于表(.ark和.scp),是二进制还是文本模型由说明符中的”,t”选项控制。

上面命令是看模型包含哪种信息。要更多细节和模型的表示意义,则看HMM topology and transition modeling

有一个很重要的点,Kaldi中的pdf是用数字id表示的,从0开始。pdf在HTK中是没有名字的。而且.mdl模型文件是没有足够的信息来对上下文相关音素和pdf-id进行映射的。要看这些信息,需要查看树文件,执行命令:

copy-tree --binary=false exp/mono/tree - | less

格式示例:

ContextDependency 3 1 ToPdf TE 1 49 ( NULL SE -1 [ 0 1 ]
{ SE -1 [ 0 ]
{ CE 0 CE 54 } 
CE 48 } 
SE -1 [ 0 1 ]
{ SE -1 [ 0 ]
{ SE 0 [ 1 ]
{ SE 2 [ 4 19 36 ]
{ CE 1 CE 1612 } 
SE 2 [ 4 19 36 ]
{ SE 0 [ 16 29 45 ]
{ CE 110 SE 0 [ 21 ]
{ CE 748 SE 0 [ 9 12 13 22 ]
{ SE 0 [ 9 ]
{ CE 623 CE 1599 }

这是单音素的“树”,单音素的“树”很小——它没有任何分裂。尽管这种树格式并不是很人性化,还是解释下:ToPdf后面,包含了多类型的EventMap对象,有个key,value的键值对,代表 上下文和HMM状态 ,映射到一个整型的pdf ID。来自EventMap的是类型ConstantEventMap(表示树的叶子),TableEventMap(表示某种查找表)和SplitEventMap(表示树分割)。在文件exp / mono / tree中,“CE”是ConstantEventMap的标记(对应于树的叶子),“TE”是TableEventMap的标记(没有“SE”或SplitEventMap,因为这个是单声道的情况)。 “TE 0 49”是TableEventMap的开始,它在key 0 上“分割”(表示在单声道情况下长度为1的音素上下文向量中的第0个音素位置)。在括号中,接下里是EventMap类型的49个对象。第一个为NULL,表示指向EventMap的零指针,因为将phone-id中的0留给“epsilon”。例子中非NULL对象是字符串“TE -1 3(CE 33 CE 34 CE 35)”,它表示在键-1上分割的TableEventMap。该键表示拓扑文件中指定的PdfClass,在本例中与HMM状态索引相同。该音素具有3个HMM状态,因此分配给该键的值可以取值0,1或2.括号内有三个类型为ConstantEventMap的对象,每个对象都表示树的叶子。

再来看看exp/mono/ali.1.gz文件(对齐文件)

copy-int-vector "ark:gunzip -c exp/mono/ali.1.gz|" ark,t:- | head -n 2

timit数据集的ali.1.gz文件格式示例如下:

faem0_si1392 2 4 3 3 3 3 3 3 6 5 5 5 5 38 37 37 37 40 42 218 217 217 217 217 217 217 217 217 217 217 217 220 219 222 221 221 248 247 247 247 247 247 247 250 252 176 
……

这是训练数据的viterbi对齐文件;每个训练文件都有对应的一行。再对比看上面说的tree文件,找到数字最大的pdf-id(tree文件最后一个数字,比如timit中最大的数字是(48音素*3-1 = 143)),会发现对齐文件中的数字比最大的pdf-id还要大得多。为什么?因为对齐文件中的数字不包含pdf-id,而是包含一个稍微更细粒度的标识符,我们称之为“transition-id”。这也对音素的原型拓扑中的音素和状态转移对应地进行编码。

可以通过”show-transitions”命令来看转移-id的信息。
如果有占用计算文件?.occs文件,可以把这个文件当成第二个参数,这样会有更多信息

show-transitions data/lang/phones.txt exp/mono/0.mdl

得到结果如下:

Transition-state 1: phone = sil hmm-state = 0 pdf = 0
 Transition-id = 1 p = 0.84957 [self-loop]
 Transition-id = 2 p = 0.15043 [0 -> 1]
Transition-state 2: phone = sil hmm-state = 1 pdf = 1
 Transition-id = 3 p = 0.87305 [self-loop]
 Transition-id = 4 p = 0.12695 [1 -> 2]
Transition-state 3: phone = sil hmm-state = 2 pdf = 2
 Transition-id = 5 p = 0.690806 [self-loop]
 Transition-id = 6 p = 0.309194 [2 -> 3]

可以看到转移-id和音素及其状态的对应关系。

查看更人性化的对齐文件形式

show-alignments data/lang/phones.txt exp/mono/0.mdl "ark:gunzip -c exp/mono/ali.1.gz |" | less

会把同一个音素的状态用[ ]圈起来

更多信息像 HMM拓扑,转移-id,转移模型等东西,可以看 HMM topology and transition modeling.

查看训练的处理过程,输入命令:

grep Overall exp/mono/log/acc.{?,??}.{?,??}.log

可以看到每次迭代的声学似然。
还可以查看update.*.log文件看更新log的信息,命令如下:

grep Overall exp/mono/log/update.*.log

效果如下:

log/update.0.log:LOG (gmm-est[5.2]:main():gmm-est.cc:102) Transition model update: Overall 0.0380546 log-like improvement per frame over 1.12482e+06 frames.
log/update.0.log:LOG (gmm-est[5.2]:main():gmm-est.cc:113) GMM update: Overall 0.7529 objective function improvement per frame over 1.12482e+06 frames
log/update.0.log:LOG (gmm-est[5.2]:main():gmm-est.cc:116) GMM update: Overall avg like per frame = -111.162 over 1.12482e+06 frames.

当单音素训练完成后,可以测试单音素的解码过程。
在解码前,需要创建词图。输入命令:

utils/mkgraph.sh --mono data/lang exp/mono exp/mono/graph

查看utils / mkgraph.sh调用的程序。 这些程序的名字很多都以“fst”(例如fsttablecompose)开头,其中大多数程序实际上并不是来自OpenFst发行版。 Kaldi创建了一些自己的FST操作程序。 可以通过后面的命令找到这些程序的位置。 使用在utils / mkgraph.sh中调用的任意程序(例如fstdeterminizestar)。 然后输入:

which fstdeterminizestar

可以找到程序位置。

有不同版本的程序的原因主要是因为在语音识别中使用FST有一点差别(较少的AT&T-ish)。 例如,“fstdeterminizestar”对应于删除ε弧的“classical”。 有关更多信息,查看Decoding graph construction in Kaldi

在图创建过程之后,我们可以通过以下方式开始单音子解码:

steps/decode.sh --config conf/decode.config --nj 20 --cmd "$decode_cmd" \
  exp/mono/graph data/test exp/mono/decode

可以看看一些解码的输出文件

less exp/mono/decode/log/decode.2.log 

可以看到屏幕上输出了转录的文件(生成的标注)。转录文件的文本形式只在日志信息中出现:程序实际上的输出在exp/mono/decode/scoring/2.tra中。这些tra文件代表了使用解码过程的语言模型(LM)。LM的范围默认使用2-13。
查看实际的解码序列,输入命令:

utils/int2sym.pl -f 2- data/lang/words.txt exp/mono/decode/scoring/2.tra

还有个脚本叫sym2int,可以转回来:

utils/int2sym.pl -f 2- data/lang/words.txt exp/mono/decode/scoring/2.tra | \
utils/sym2int.pl -f 2- data/lang/words.txt 

The -f 2- 选项的意思是避免把utt-id转换成int类型。输入命令:

tail exp/mono/decode/log/decode.2.log 

会打印末尾一些有用的总结信息,包括实时因子和每帧的平均对数似然。实时因子一般是0.2到0.3之间(比实时更快)。取决于CPU和用了多少线程执行任务和一些其它因素。脚本并行运行20个作业,如果机器少于20个内核就会慢得多。注意解码过程中用了很宽容的剪枝(值为20),以获取更精确的结果。在典型的大词汇量的语音识别集中,beam会小得多(大概13)。
再次查看日志文件的顶部,并专注于命令行。 可选参数位于目标参数之前(这是必需的)。输入

gmm-decode-faster

查看使用信息,并将参数和log文件看到的参数匹配。回想 “rspecifier” 是专门用来读表的字符串之一, “wspecifier”是专门用来写入表的一个。
单音子系统介绍结束,后续可看三音子系统。
Up: Kaldi tutorial
Previous: Overview of the distribution
Next: Reading and modifying the code

  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值