前言:科大訊飛的新版離線語(yǔ)音聽(tīng)寫(xiě),由于官網(wǎng)demo是kt語(yǔ)言開(kāi)發(fā)的,咱也看不懂kt,搜遍了全網(wǎng)也沒(méi)看到一個(gè)java版的新版離線語(yǔ)音demo,現(xiàn)記錄下,留給有緣人參考?。。。?!畢竟咱在這上面遇到了不少的坑。如果能留言指正,那就更好了。
實(shí)測(cè)一點(diǎn)問(wèn)題都沒(méi)
一、先把官網(wǎng)Demo中resource下的文件放到sdk目錄下,示例如下
一、Activity簡(jiǎn)單布局 加幾個(gè)語(yǔ)音聽(tīng)寫(xiě)的監(jiān)聽(tīng)回調(diào)
package com.mhzk.xunfeitest;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.iflytek.aikit.core.AiHandle;
import com.iflytek.aikit.core.AiStatus;
public class MainActivity extends AppCompatActivity implements AbilityCallback{
private String TAG = "內(nèi)容初始化";
private AiStatus state;
private AiHandle handle;
private int REQUEST_STORAGE_PERMISSION = 100;
private TextView start;
private TextView content;
private AudioRecordUtil instance;
private StringBuffer strBuffer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
start = findViewById(R.id.one);
content = findViewById(R.id.three);
instance = AudioRecordUtil.getInstance();
instance.initSDK(this);
AudioRecordUtil.setCallBack(this);
start.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(start.getText().toString().equals("開(kāi)始錄音")){
instance.start(MainActivity.this);
}else {
instance.stop();
}
}
});
requestStoragePermission(this);
}
/**
* 查看當(dāng)前設(shè)備是否有存儲(chǔ)權(quán)限:
* 沒(méi)有:請(qǐng)求獲取權(quán)限
* 有:復(fù)制當(dāng)前項(xiàng)目assets下的xtts文件夾到設(shè)備根目錄下(語(yǔ)音合成所必須的文件)
* @param context
*/
private void requestStoragePermission(Context context) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_STORAGE_PERMISSION);
}
}
/**
* 請(qǐng)求獲取存儲(chǔ)權(quán)限
* @param requestCode
* @param permissions
* @param grantResults
*/
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_STORAGE_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.i(TAG, "onRequestPermissionsResult: permission granted");
//再次判斷存儲(chǔ)權(quán)限是否已授予
boolean permission = FileUtils.hasStoragePermission(getApplicationContext());
if (!permission) {
Toast.makeText(getApplicationContext(), "沒(méi)有存儲(chǔ)權(quán)限,請(qǐng)重新獲取!", Toast.LENGTH_SHORT).show();
return;
}
// 應(yīng)用具有存儲(chǔ)權(quán)限
Log.i(TAG,"成功獲取存儲(chǔ)權(quán)限!");
//判斷xtts文件是否存在,不存在則復(fù)制,存在則忽略
FileUtils.createXttsDirAndCopyFile(getApplicationContext());
} else {
Log.i(TAG, "onRequestPermissionsResult: permission denied");
Toast.makeText(this, "You Denied Permission", Toast.LENGTH_SHORT).show();
}
}
}
/**
* 開(kāi)始
*/
@Override
public void onAbilityBegin() {
start.setText("暫停錄音");
content.setText("內(nèi)容展示");
}
/**
*能力結(jié)果輸出
* @param result 結(jié)果
*/
@Override
public void onAbilityResult(String result) {
String s = content.getText().toString();
strBuffer = new StringBuffer(s);
strBuffer.append("\n"+result );
String value = strBuffer.toString();
runOnUiThread(new Runnable() {
@Override
public void run() {
content.setText(value);
}
});
}
/**
*結(jié)束
* @param code
* @param error
*/
@Override
public void onAbilityError(int code, Throwable error) {
runOnUiThread(new Runnable() {
@Override
public void run() {
start.setText("開(kāi)始錄音");
}
});
instance.stop();
}
/**
* 能力結(jié)束
*/
@Override
public void onAbilityEnd() {
runOnUiThread(new Runnable() {
@Override
public void run() {
start.setText("開(kāi)始錄音");
}
});
}
}
二、AudioRecord進(jìn)行音頻錄制并寫(xiě)入本地創(chuàng)建的pcm文件
????????寫(xiě)入過(guò)程中把流數(shù)據(jù)復(fù)制一份傳給sdk做文字轉(zhuǎn)換
package com.mhzk.xunfeitest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Environment;
import android.util.Log;
import androidx.annotation.NonNull;
import com.iflytek.aikit.core.AiAudio;
import com.iflytek.aikit.core.AiEvent;
import com.iflytek.aikit.core.AiHandle;
import com.iflytek.aikit.core.AiHelper;
import com.iflytek.aikit.core.AiListener;
import com.iflytek.aikit.core.AiRequest;
import com.iflytek.aikit.core.AiResponse;
import com.iflytek.aikit.core.AiStatus;
import com.iflytek.aikit.core.AuthListener;
import com.iflytek.aikit.core.DataStatus;
import com.iflytek.aikit.core.ErrType;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class AudioRecordUtil {
private static final String TAG = "AudioPlayByKeyUtils";
//設(shè)置音頻采樣率,44100是目前的標(biāo)準(zhǔn),但是某些設(shè)備仍然支持22050,16000,11025
private final int sampleRateInHz = 16000;
//設(shè)置音頻的錄制的聲道CHANNEL_IN_STEREO為雙聲道,CHANNEL_CONFIGURATION_MONO為單聲道
private final int channelConfig = AudioFormat.CHANNEL_IN_MONO;
//音頻數(shù)據(jù)格式:PCM 16位每個(gè)樣本。保證設(shè)備支持。PCM 8位每個(gè)樣本。不一定能得到設(shè)備支持。
private final int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
//錄制狀態(tài)
private boolean recorderState = true;
private byte[] buffer;
private static AudioRecord audioRecord;
private static AudioRecordUtil audioRecordUtil = new AudioRecordUtil();
private static AiHandle handle;
private File recordFile;
private AtomicBoolean atomicBoolean = new AtomicBoolean();
private byte[] lockArray = new byte[0];
private int recordMinBufferSize;
//SDK初始化
public void initSDK(Context context) {
try {
//外部存儲(chǔ)絕對(duì)路徑
File externalStorageDirectory = Environment.getExternalStorageDirectory();
// 初始化參數(shù)構(gòu)建
AiHelper.Params params = AiHelper.Params.builder()
.appId(context.getString(R.string.appId))
.apiKey(context.getString(R.string.apiKey))
.apiSecret(context.getString(R.string.apiSecret))
.workDir("/sdcard/iflytekAikit")//SDK工作路徑,這里為絕對(duì)路徑
.authInterval(333) //授權(quán)更新間隔
.build();
// 初始化
AiHelper.getInst().init(context, params);
// 注冊(cè)SDK 初始化狀態(tài)監(jiān)聽(tīng)
AiHelper.getInst().registerListener(coreListener);
// 注冊(cè)能力結(jié)果監(jiān)聽(tīng) R.string.enginID 為離線的語(yǔ)音聽(tīng)寫(xiě)ID,寫(xiě)死就好 ee62fa27c
AiHelper.getInst().registerListener(context.getString(R.string.enginID), aiRespListener);
} catch (Exception e) {
Log.e(TAG, "語(yǔ)音合成初始化出現(xiàn)異常" + e.getMessage());
}
}
public static AudioRecordUtil getInstance() {
return audioRecordUtil;
}
private AudioRecordUtil() {
init();
}
@SuppressLint("MissingPermission")
private void init() {
recordMinBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
//指定 AudioRecord 緩沖區(qū)大小
buffer = new byte[recordMinBufferSize];
//根據(jù)錄音參數(shù)構(gòu)造AudioRecord實(shí)體對(duì)象
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConfig,
audioFormat, recordMinBufferSize);
}
/**
* 開(kāi)始錄制
*/
public void start(Context context) {
//已初始化則略過(guò)
// initSDK(context);
//能力逆初始化, 部分能力,比如語(yǔ)種切換的時(shí)候 需要逆初始化
AiHelper.getInst().engineUnInit(context.getString(R.string.enginID));
int ret = -1;
ret = AiHelper.getInst().engineInit(context.getString(R.string.enginID));
if (ret != 0) {
abilityCallback.onAbilityError(ret, new Throwable("引擎初始化失?。?ret"));
return;
}
int[] indexs0 = {0};
int[] indexs1 = {1};
ret = AiHelper.getInst()
.specifyDataSet(context.getString(R.string.enginID), "PPROC_NOT_REP", indexs0);
if (ret != 0) {
abilityCallback.onAbilityError(ret, new Throwable("open esr specifyDataSet 失敗:$ret"));
return;
}
ret = AiHelper.getInst()
.specifyDataSet(context.getString(R.string.enginID), "PPROC_REPLACE",indexs1);
if (ret != 0) {
abilityCallback.onAbilityError(ret, new Throwable("open esr specifyDataSet 失?。?ret"));
return;
}
//音量及播報(bào)人等參數(shù)設(shè)置
AiRequest.Builder paramBuilder = audioParam();
handle = AiHelper.getInst().start(context.getString(R.string.enginID), paramBuilder.build(), null);
atomicBoolean.set(true);
if (!handle.isSuccess()) {
Log.e(TAG, "ERROR::START | handle code:" + handle.getCode());
return;
}
if (audioRecord.getState() == AudioRecord.RECORDSTATE_STOPPED) {
recorderState = true;
audioRecord.startRecording();
abilityCallback.onAbilityBegin();
String absolutePath = MyApp.mApplication.getExternalCacheDir().getAbsolutePath();
recordFile = new File(absolutePath + "/" + System.currentTimeMillis() + ".pcm");
}else {
init();
recorderState = true;
audioRecord.startRecording();
abilityCallback.onAbilityBegin();
String absolutePath = MyApp.mApplication.getExternalCacheDir().getAbsolutePath();
recordFile = new File(absolutePath + "/" + System.currentTimeMillis() + ".pcm");
}
new RecordThread().start();
}
/**
* 停止錄制
*/
public void stop() {
recorderState = false;
if (audioRecord.getState() == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord.stop();
}
audioRecord.release();
int ret = AiHelper.getInst().end(handle);
if (ret != 0) {
String error = "end failed" + ret;
Log.e(TAG, error);
}
}
private class RecordThread extends Thread {
@Override
public void run() {
//輸出流
OutputStream os = null;
try {
os = new FileOutputStream(recordFile);
// BufferedOutputStream bos = new BufferedOutputStream(os);
// DataOutputStream dos = new DataOutputStream(bos);
while (recorderState) {
int read = audioRecord.read(buffer, 0, buffer.length);
for (int i = 0; i < read; i++) {
// dos.writeShort(buffer[i]);
}
if (read == recordMinBufferSize) {
AiStatus status = AiStatus.CONTINUE;
if (atomicBoolean.get()) {
status = AiStatus.BEGIN;
atomicBoolean.set(false);
}
writeData(buffer, status);
} else {
byte[] copy = new byte[read];
System.arraycopy(buffer, 0, copy, 0, read);
AiStatus status = AiStatus.CONTINUE;
if (atomicBoolean.get()) {
status = AiStatus.BEGIN;
atomicBoolean.set(false);
}
writeData(copy, status);
}
os.write(buffer);
}
os.flush();
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 寫(xiě)入音頻數(shù)據(jù)
*
* @param status 送入的數(shù)據(jù)的狀態(tài),告訴引擎送入的首幀數(shù)據(jù)、中間數(shù)據(jù)、還是尾幀
*/
private void writeData(byte[] audio, AiStatus status) {
if (handle == null) {
return;
}
synchronized (lockArray) {
AiRequest.Builder dataBuilder = AiRequest.builder();
AiAudio.Holder holder = AiAudio.get("PCM").data(audio);
holder.status(status);
dataBuilder.payload(holder.valid());
int ret = AiHelper.getInst().write(dataBuilder.build(), handle);
if (ret != 0) {
destory();
Log.w(TAG, "writeData is error => $ret");
} else {
ret = AiHelper.getInst().read("ee62fa27c", handle);
if (ret != 0) {
Log.w(TAG, "read error code => $ret");
destory();
} else {
Log.w(TAG, "read success code => $ret");
}
}
}
}
/**
* SDK監(jiān)聽(tīng)回調(diào)
*/
private static AuthListener coreListener = new AuthListener() {
@Override
public void onAuthStateChange(final ErrType type, final int code) {
Log.i(TAG, "core listener code:" + code);
switch (type) {
case AUTH:
Log.i(TAG, "SDK狀態(tài):授權(quán)結(jié)果碼" + code);
break;
case HTTP:
Log.i(TAG, "SDK狀態(tài):HTTP認(rèn)證結(jié)果" + code);
break;
default:
Log.i(TAG, "SDK狀態(tài):其他錯(cuò)誤");
}
}
};
/**
* 能力監(jiān)聽(tīng)回調(diào)
*/
private static AiListener aiRespListener = new AiListener() {
//獲取合成結(jié)果,封裝到緩存數(shù)組中
@Override
public void onResult(int handleID, List<AiResponse> outputData, Object usrContext) {
if (outputData == null || outputData.isEmpty()) {
return;
}
if (null != outputData && outputData.size() > 0) {
for (int i = 0; i < outputData.size(); i++) {
byte[] bytes = outputData.get(i).getValue();
String key = outputData.get(i).getKey();
if (key.contains("plain") || key.contains("pgs")) {
try {
String s = new String(bytes, "GBK");
Log.e(TAG, key + " " + s);
abilityCallback.onAbilityResult(key+ " "+s);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
if (key.contains("plain")) {
stopAsr();
}
}
}
if (outputData.get(0).getStatus() == DataStatus.END.getValue()) {
stopAsr();
}
}
}
@Override
public void onEvent(int handleID, int event, List<AiResponse> eventData, Object usrContext) {
if (event == AiEvent.EVENT_UNKNOWN.getValue()) {
}
if (event == AiEvent.EVENT_START.getValue()) {
}
if (event == AiEvent.EVENT_END.getValue()) {
if (handle != null) {
int rets = AiHelper.getInst().end(handle);
if (rets != 0) {
String error = "end failed" + rets;
Log.e(TAG, error);
}
}
}
if (event == AiEvent.EVENT_PROGRESS.getValue()) {
}
}
@Override
public void onError(int handleID, int err, String msg, Object usrContext) {
if (handle != null) {
int rets = AiHelper.getInst().end(handle);
if (rets != 0) {
String error = "end failed" + rets;
Log.e(TAG, error);
}
}
}
};
/**
* 音量及播報(bào)人等參數(shù)設(shè)置
*/
@NonNull
private static AiRequest.Builder audioParam() {
AiRequest.Builder paramBuilder = AiRequest.builder();
paramBuilder.param("lmLoad", true);
paramBuilder.param("vadLoad", true);
paramBuilder.param("puncLoad", true);
paramBuilder.param("numLoad", true);
paramBuilder.param("postprocOn", true);
paramBuilder.param("lmOn", true);
paramBuilder.param("vadOn", true);
paramBuilder.param("vadLinkOn", false);
paramBuilder.param("vadNeed", true);
paramBuilder.param("vadThreshold", 0.1332);
paramBuilder.param("vadEnergyThreshold", 9);
return paramBuilder;
}
/**
* 釋放資源
*/
public static void destory() {
stopAsr();
}
/**
* 停止語(yǔ)音識(shí)別
*/
private static void stopAsr() {
if (audioRecord != null) {
if (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
audioRecord.stop();
}
}
audioRecord.release();
int ret = AiHelper.getInst().end(handle);
if (ret == 0) {
abilityCallback.onAbilityEnd();
} else {
abilityCallback.onAbilityError(ret, new Throwable("aiHandle end error"));
}
}
private static AbilityCallback abilityCallback;
public static void setCallBack(AbilityCallback callBack) {
abilityCallback = callBack;
}
}
接口類(lèi)
package com.mhzk.xunfeitest;
public interface AbilityCallback {
/**
* 開(kāi)始
*/
void onAbilityBegin();
/**
* 能力結(jié)果輸出
*
* @param result 結(jié)果
*/
void onAbilityResult(String result);
/**
* 結(jié)束
*
* @param code
* @param error
*/
void onAbilityError(int code, Throwable error);
/**
* 能力結(jié)束
*/
void onAbilityEnd();
}
Activity中權(quán)限使用類(lèi),用不用都行,看自己
package com.mhzk.xunfeitest;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* 訊飛語(yǔ)音合成文件復(fù)制公共功能
* 以下五個(gè)文件:
* e3fe94474_1.0.0_xTTS_CnCn_xiaoyan_2018_arm.irf
* e4b08c6f3_1.0.0_xTTS_CnCn_xiaofeng_2018_fix_arm.dat
* e4caee636_1.0.2_xTTS_CnCn_front_Emb_arm_2017.irf
* e05d571cc_1.0.0_xTTS_CnCn_xiaoyan_2018_fix_arm.dat
* ebdbd61ae_1.0.0_xTTS_CnCn_xiaofeng_2018_arm.irf
*/
public class FileUtils {
private static final String TAG = "FileUtils";
// 獲取外部存儲(chǔ)路徑
public static String getExternalStoragePath() {
return Environment.getExternalStorageDirectory().getAbsolutePath();
}
// 創(chuàng)建xtts目錄
public static void createDirectory(String directoryPath) {
File directory = new File(directoryPath);
if (!directory.exists()) {
if (directory.mkdirs()) {
Log.d(TAG, "Directory created: " + directoryPath);
} else {
Log.e(TAG, "Failed to create directory: " + directoryPath);
}
} else {
Log.d(TAG, "Directory already exists: " + directoryPath);
}
}
// 判斷目錄是否為空
public static boolean isDirectoryEmpty(String directoryPath) {
File directory = new File(directoryPath);
if (directory.exists() && directory.isDirectory()) {
File[] files = directory.listFiles();
return files == null || files.length == 0;
}
return true;
}
// 遞歸復(fù)制文件
public static void copyFiles(Context context, String sourceDir, String destinationDir) throws IOException {
AssetManager assetManager = context.getAssets();
String[] files = assetManager.list(sourceDir);
if (files != null && files.length > 0) {
createDirectory(destinationDir);
for (String fileName : files) {
String sourcePath = sourceDir + File.separator + fileName;
String destinationPath = destinationDir + File.separator + fileName;
if (assetManager.list(sourcePath).length > 0) {
// 如果是目錄,遞歸復(fù)制目錄
copyFiles(context, sourcePath, destinationPath);
} else {
// 如果是文件,復(fù)制文件
copyFile(context, sourcePath, destinationPath);
}
}
}
}
// 復(fù)制文件
public static void copyFile(Context context, String sourcePath, String destinationPath) throws IOException {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
inputStream = context.getAssets().open(sourcePath);
outputStream = new FileOutputStream(destinationPath);
byte[] buffer = new byte[4096];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
Log.d(TAG, "File copied: " + destinationPath);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
Log.e(TAG, "Failed to close input stream", e);
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
Log.e(TAG, "Failed to close output stream", e);
}
}
}
}
/**
* 創(chuàng)建訊飛語(yǔ)音合成所必須的目錄:xtts并復(fù)制音頻文件
* @param context
*/
public static void createXttsDirAndCopyFile(Context context){
// 獲取外部存儲(chǔ)路徑
String externalStoragePath = FileUtils.getExternalStoragePath();
String xttsFolderPath = externalStoragePath + File.separator + context.getString(R.string.dir);
// 創(chuàng)建xtts文件夾
FileUtils.createDirectory(xttsFolderPath);
// 判斷xtts文件夾是否為空
if (FileUtils.isDirectoryEmpty(xttsFolderPath)) {
// 復(fù)制assets目錄下的xtts文件夾中的所有文件到外部存儲(chǔ)的xtts文件夾中
try {
FileUtils.copyFiles(context, context.getString(R.string.dir), xttsFolderPath);
} catch (IOException e) {
Log.e(TAG, "文件復(fù)制失敗"+e.getMessage());
}
} else {
// xtts文件夾不為空
Log.d(TAG, "xtts folder is not empty. Skipping the operation.");
}
}
public static boolean hasStoragePermission(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int permissionResult = context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE);
return permissionResult == PackageManager.PERMISSION_GRANTED;
}
return true;
}
}
Activity的布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/one"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="開(kāi)始錄音"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_marginTop="20dp"
android:id="@+id/three"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="內(nèi)容展示"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
?要demo的可以私信文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-835530.html
?文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-835530.html
到了這里,關(guān)于科大訊飛 新版AIkit 離線語(yǔ)音聽(tīng)寫(xiě) Java 版本的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!