音频模型调整
This commit is contained in:
@@ -180,6 +180,8 @@ public class PasswordUtil {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ public class ServiceManager {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ public class AliyunConfig {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ public class SiliconFlowTtsController {
|
|||||||
String fileExtension = originalFilename != null && originalFilename.contains(".")
|
String fileExtension = originalFilename != null && originalFilename.contains(".")
|
||||||
? originalFilename.substring(originalFilename.lastIndexOf("."))
|
? originalFilename.substring(originalFilename.lastIndexOf("."))
|
||||||
: ".mp3";
|
: ".mp3";
|
||||||
String uniqueFileName = "audio/" + UUID.randomUUID().toString() + fileExtension;
|
String uniqueFileName = "/" + UUID.randomUUID().toString() + fileExtension;
|
||||||
|
|
||||||
// 上传文件到MinIO
|
// 上传文件到MinIO
|
||||||
String minioUrl = minIOService.uploadFileWithName(file, uniqueFileName);
|
String minioUrl = minIOService.uploadFileWithName(file, uniqueFileName);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
import com.rj.entity.TtsRequestLog;
|
import com.rj.entity.TtsRequestLog;
|
||||||
import com.rj.mapper.TtsRequestLogMapper;
|
import com.rj.mapper.TtsRequestLogMapper;
|
||||||
import com.rj.service.ITtsRequestLogService;
|
import com.rj.service.ITtsRequestLogService;
|
||||||
|
import com.rj.dto.AsrRequest;
|
||||||
|
import com.rj.dto.AsrResponse;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -13,7 +15,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ public class TtsRequestLogController {
|
|||||||
public ResponseEntity<Map<String, Object>> getTtsRequestLogs(
|
public ResponseEntity<Map<String, Object>> getTtsRequestLogs(
|
||||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum,
|
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum,
|
||||||
@Parameter(description = "每页大小") @RequestParam(defaultValue = "10") Integer pageSize,
|
@Parameter(description = "每页大小") @RequestParam(defaultValue = "10") Integer pageSize,
|
||||||
|
@Parameter(description = "音乐名称") @RequestParam(required = false) String audioName,
|
||||||
@Parameter(description = "状态") @RequestParam(required = false) String status,
|
@Parameter(description = "状态") @RequestParam(required = false) String status,
|
||||||
@Parameter(description = "模型") @RequestParam(required = false) String model,
|
@Parameter(description = "模型") @RequestParam(required = false) String model,
|
||||||
@Parameter(description = "创建人姓名") @RequestParam(required = false) String creatorName,
|
@Parameter(description = "创建人姓名") @RequestParam(required = false) String creatorName,
|
||||||
@@ -54,6 +56,9 @@ public class TtsRequestLogController {
|
|||||||
QueryWrapper<TtsRequestLog> queryWrapper = new QueryWrapper<>();
|
QueryWrapper<TtsRequestLog> queryWrapper = new QueryWrapper<>();
|
||||||
|
|
||||||
// 添加查询条件
|
// 添加查询条件
|
||||||
|
if (audioName != null && !audioName.trim().isEmpty()) {
|
||||||
|
queryWrapper.like("audio_name", audioName);
|
||||||
|
}
|
||||||
if (status != null && !status.trim().isEmpty()) {
|
if (status != null && !status.trim().isEmpty()) {
|
||||||
queryWrapper.eq("status", status);
|
queryWrapper.eq("status", status);
|
||||||
}
|
}
|
||||||
@@ -149,4 +154,28 @@ public class TtsRequestLogController {
|
|||||||
return ResponseEntity.status(500).body(result);
|
return ResponseEntity.status(500).body(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/speech-to-text")
|
||||||
|
@Operation(summary = "语音转文本", description = "将语音文件转换为文本")
|
||||||
|
public ResponseEntity<AsrResponse> speechToText(
|
||||||
|
@Parameter(description = "语音转文本请求参数") @RequestBody AsrRequest request) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!ttsRequestLogService.isAsrServiceAvailable()) {
|
||||||
|
return ResponseEntity.status(500).body(AsrResponse.error("ASR服务不可用,请检查配置"));
|
||||||
|
}
|
||||||
|
|
||||||
|
AsrResponse response = ttsRequestLogService.speechToText(request);
|
||||||
|
|
||||||
|
if (response.isSuccess()) {
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.status(500).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("语音转文本失败: {}", e.getMessage(), e);
|
||||||
|
return ResponseEntity.status(500).body(AsrResponse.error("语音转文本失败: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -402,6 +402,8 @@ public class MenuController {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -372,6 +372,8 @@ public class RoleController {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -378,6 +378,8 @@ public class UserRoleController {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ public class DifyWorkflowResponseDto {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ public interface CustomerProfileAnalysisMapper extends BaseMapper<CustomerProfil
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.rj.service;
|
|||||||
|
|
||||||
import com.rj.entity.TtsRequestLog;
|
import com.rj.entity.TtsRequestLog;
|
||||||
import com.rj.utils.MinIOUrlGenerator;
|
import com.rj.utils.MinIOUrlGenerator;
|
||||||
|
import com.rj.dto.AsrRequest;
|
||||||
|
import com.rj.dto.AsrResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TTS请求日志服务接口
|
* TTS请求日志服务接口
|
||||||
@@ -41,4 +43,19 @@ public interface ITtsRequestLogService {
|
|||||||
* @return 是否设置成功
|
* @return 是否设置成功
|
||||||
*/
|
*/
|
||||||
boolean setShortUrlByAudioName(String audioName);
|
boolean setShortUrlByAudioName(String audioName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语音转文本
|
||||||
|
*
|
||||||
|
* @param request ASR请求
|
||||||
|
* @return ASR响应
|
||||||
|
*/
|
||||||
|
AsrResponse speechToText(AsrRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查ASR服务是否可用
|
||||||
|
*
|
||||||
|
* @return 是否可用
|
||||||
|
*/
|
||||||
|
boolean isAsrServiceAvailable();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ public interface ICustomerProfileAnalysisService extends IService<CustomerProfil
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,8 @@ public class CustomerProfileAnalysisServiceImpl extends ServiceImpl<CustomerProf
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
package com.rj.service.impl;
|
package com.rj.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.dashscope.audio.asr.transcription.*;
|
||||||
import com.rj.entity.TtsRequestLog;
|
import com.rj.entity.TtsRequestLog;
|
||||||
import com.rj.mapper.TtsRequestLogMapper;
|
import com.rj.mapper.TtsRequestLogMapper;
|
||||||
import com.rj.service.ITtsRequestLogService;
|
import com.rj.service.ITtsRequestLogService;
|
||||||
import com.rj.utils.MinIOUrlGenerator;
|
import com.rj.utils.MinIOUrlGenerator;
|
||||||
|
import com.rj.dto.AsrRequest;
|
||||||
|
import com.rj.dto.AsrResponse;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TTS请求日志服务实现类
|
* TTS请求日志服务实现类
|
||||||
@@ -27,6 +34,15 @@ public class TtsRequestLogServiceImpl implements ITtsRequestLogService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private MinIOUrlGenerator urlGenerator;
|
private MinIOUrlGenerator urlGenerator;
|
||||||
|
|
||||||
|
@Value("${dashscope.api.key}")
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
|
@Value("${asr.enabled:true}")
|
||||||
|
private boolean asrEnabled;
|
||||||
|
|
||||||
|
@Value("${asr.default-model:paraformer-v2}")
|
||||||
|
private String defaultAsrModel;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean saveTtsRequestLog(TtsRequestLog requestLog) {
|
public boolean saveTtsRequestLog(TtsRequestLog requestLog) {
|
||||||
try {
|
try {
|
||||||
@@ -52,7 +68,7 @@ public class TtsRequestLogServiceImpl implements ITtsRequestLogService {
|
|||||||
@Override
|
@Override
|
||||||
public boolean setShortUrlByAudioName(MinIOUrlGenerator.UrlInfo urlInfo ) {
|
public boolean setShortUrlByAudioName(MinIOUrlGenerator.UrlInfo urlInfo ) {
|
||||||
try {
|
try {
|
||||||
if (urlInfo == null || urlInfo.getFileName().trim().isEmpty()) {
|
if (urlInfo == null || urlInfo.getFileName() == null || urlInfo.getFileName().trim().isEmpty()) {
|
||||||
log.error("音频名称不能为空");
|
log.error("音频名称不能为空");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -92,4 +108,131 @@ public class TtsRequestLogServiceImpl implements ITtsRequestLogService {
|
|||||||
public boolean setShortUrlByAudioName(String audioName) {
|
public boolean setShortUrlByAudioName(String audioName) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AsrResponse speechToText(AsrRequest request) {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!isAsrServiceAvailable()) {
|
||||||
|
return AsrResponse.error("ASR服务不可用,请检查配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getAudioUrl() == null || request.getAudioUrl().trim().isEmpty()) {
|
||||||
|
return AsrResponse.error("音频文件URL不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认值
|
||||||
|
if (request.getModel() == null || request.getModel().trim().isEmpty()) {
|
||||||
|
request.setModel(defaultAsrModel);
|
||||||
|
}
|
||||||
|
if (request.getFormat() == null || request.getFormat().trim().isEmpty()) {
|
||||||
|
request.setFormat("wav");
|
||||||
|
}
|
||||||
|
if (request.getSampleRate() == null) {
|
||||||
|
request.setSampleRate(16000);
|
||||||
|
}
|
||||||
|
if (request.getEnablePunctuation() == null) {
|
||||||
|
request.setEnablePunctuation(true);
|
||||||
|
}
|
||||||
|
if (request.getEnableNumberConversion() == null) {
|
||||||
|
request.setEnableNumberConversion(true);
|
||||||
|
}
|
||||||
|
if (request.getEnableSpeakerDiarization() == null) {
|
||||||
|
request.setEnableSpeakerDiarization(false);
|
||||||
|
}
|
||||||
|
if (request.getSpeakerCount() == null) {
|
||||||
|
request.setSpeakerCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("开始语音识别 - 音频URL: {}, 模型: {}, 格式: {}, 采样率: {}",
|
||||||
|
request.getAudioUrl(), request.getModel(), request.getFormat(), request.getSampleRate());
|
||||||
|
|
||||||
|
// 创建转写请求参数
|
||||||
|
TranscriptionParam param = TranscriptionParam.builder()
|
||||||
|
.apiKey(apiKey)
|
||||||
|
.model(request.getModel())
|
||||||
|
.fileUrls(Arrays.asList(request.getAudioUrl()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 创建转写对象
|
||||||
|
Transcription transcription = new Transcription();
|
||||||
|
|
||||||
|
// 提交转写请求
|
||||||
|
TranscriptionResult result = transcription.asyncCall(param);
|
||||||
|
String taskId = result.getTaskId();
|
||||||
|
String requestId = result.getRequestId();
|
||||||
|
|
||||||
|
log.info("ASR任务已提交 - TaskId: {}, RequestId: {}", taskId, requestId);
|
||||||
|
|
||||||
|
// 等待任务完成
|
||||||
|
TranscriptionQueryParam queryParam = TranscriptionQueryParam.FromTranscriptionParam(param, taskId);
|
||||||
|
result = transcription.wait(queryParam);
|
||||||
|
log.info("语音识别完成 - result: {} ", result);
|
||||||
|
long processingTime = System.currentTimeMillis() - startTime;
|
||||||
|
|
||||||
|
if (result.getOutput() != null) {
|
||||||
|
// 解析识别结果 - 从JSON中提取文本
|
||||||
|
String fullText = "";
|
||||||
|
Double duration = 0.0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 从输出中提取文本和时长信息
|
||||||
|
if (result.getOutput().toString().contains("\"text\"")) {
|
||||||
|
// 简单解析JSON获取文本
|
||||||
|
String outputStr = result.getOutput().toString();
|
||||||
|
int textStart = outputStr.indexOf("\"text\":\"") + 8;
|
||||||
|
int textEnd = outputStr.indexOf("\"", textStart);
|
||||||
|
if (textStart > 7 && textEnd > textStart) {
|
||||||
|
fullText = outputStr.substring(textStart, textEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取时长信息
|
||||||
|
if (result.getOutput().toString().contains("\"duration\"")) {
|
||||||
|
String outputStr = result.getOutput().toString();
|
||||||
|
int durationStart = outputStr.indexOf("\"duration\":") + 11;
|
||||||
|
int durationEnd = outputStr.indexOf(",", durationStart);
|
||||||
|
if (durationEnd == -1) {
|
||||||
|
durationEnd = outputStr.indexOf("}", durationStart);
|
||||||
|
}
|
||||||
|
if (durationStart > 10 && durationEnd > durationStart) {
|
||||||
|
try {
|
||||||
|
duration = Double.parseDouble(outputStr.substring(durationStart, durationEnd).trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
duration = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("解析ASR结果时出错: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AsrResponse.AsrResult> asrResults = new ArrayList<>();
|
||||||
|
|
||||||
|
// 创建简单的结果对象
|
||||||
|
AsrResponse.AsrResult asrResult = new AsrResponse.AsrResult();
|
||||||
|
asrResult.setText(fullText);
|
||||||
|
asrResult.setStartTime(0.0);
|
||||||
|
asrResult.setEndTime(duration);
|
||||||
|
asrResults.add(asrResult);
|
||||||
|
|
||||||
|
log.info("语音识别完成 - 识别文本长度: {}, 处理时间: {}ms", fullText.length(), processingTime);
|
||||||
|
|
||||||
|
return AsrResponse.success(fullText, asrResults, duration, processingTime, taskId, requestId);
|
||||||
|
} else {
|
||||||
|
log.warn("ASR识别结果为空 - TaskId: {}", taskId);
|
||||||
|
return AsrResponse.error("语音识别结果为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("语音识别失败: {}", e.getMessage(), e);
|
||||||
|
return AsrResponse.error("语音识别失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAsrServiceAvailable() {
|
||||||
|
return asrEnabled && apiKey != null && !apiKey.trim().isEmpty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IM
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements IR
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ public class UserRoleServiceImpl extends ServiceImpl<UserRoleMapper, UserRole> i
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ spring:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,27 @@ aliyun:
|
|||||||
# 是否启用阿里云服务
|
# 是否启用阿里云服务
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
# ASR语音识别配置
|
||||||
|
asr:
|
||||||
|
# 是否启用ASR服务
|
||||||
|
enabled: true
|
||||||
|
# 默认ASR模型
|
||||||
|
default-model: paraformer-v2
|
||||||
|
# 请求超时时间(毫秒)
|
||||||
|
timeout: 60000
|
||||||
|
# 是否启用标点符号
|
||||||
|
enable-punctuation: true
|
||||||
|
# 是否启用数字转换
|
||||||
|
enable-number-conversion: true
|
||||||
|
# 是否启用说话人分离
|
||||||
|
enable-speaker-diarization: false
|
||||||
|
# 默认说话人数量
|
||||||
|
default-speaker-count: 1
|
||||||
|
# 默认音频格式
|
||||||
|
default-format: wav
|
||||||
|
# 默认采样率
|
||||||
|
default-sample-rate: 16000
|
||||||
|
|
||||||
|
|
||||||
springdoc:
|
springdoc:
|
||||||
swagger-ui:
|
swagger-ui:
|
||||||
|
|||||||
@@ -101,6 +101,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -300,6 +300,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -238,6 +238,8 @@ public class FaceDetectImageCountTest {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -179,6 +179,8 @@ public class TtsRequestLogShortUrlTest {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -157,6 +157,8 @@ public class VideoSynthesisTempUrlTest {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ public class VideoSynthesisVideoNameTest {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user