功能:可以克隆用户的音色进行使用
This commit is contained in:
parent
451906a9ac
commit
774b12516f
Binary file not shown.
|
|
@ -328,3 +328,167 @@ def list_available_voices_for_lover(
|
|||
),
|
||||
msg="获取可用音色成功",
|
||||
)
|
||||
|
||||
|
||||
# ===== 音色克隆相关 =====
|
||||
|
||||
class VoiceCloneRequest(BaseModel):
|
||||
audio_url: str
|
||||
voice_name: str
|
||||
gender: str # male/female
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class VoiceCloneResponse(BaseModel):
|
||||
voice_id: str
|
||||
status: str
|
||||
message: str
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class VoiceCloneStatusResponse(BaseModel):
|
||||
voice_id: str
|
||||
status: str # PENDING, OK, UNDEPLOYED, FAILED
|
||||
voice_library_id: Optional[int] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@router.post("/voices/clone", response_model=ApiResponse[VoiceCloneResponse])
|
||||
def clone_voice(
|
||||
payload: VoiceCloneRequest,
|
||||
db: Session = Depends(get_db),
|
||||
user: AuthedUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
克隆音色:用户上传音频文件,系统调用 CosyVoice 克隆音色
|
||||
"""
|
||||
from ..cosyvoice_clone import create_voice_from_url
|
||||
|
||||
# 验证音色名称长度(CosyVoice 限制 prefix <= 10 字符)
|
||||
if len(payload.voice_name) > 10:
|
||||
raise HTTPException(status_code=400, detail="音色名称不能超过10个字符")
|
||||
|
||||
# 验证性别
|
||||
if payload.gender not in ["male", "female"]:
|
||||
raise HTTPException(status_code=400, detail="性别必须是 male 或 female")
|
||||
|
||||
try:
|
||||
# 调用克隆服务
|
||||
voice_id = create_voice_from_url(
|
||||
audio_url=payload.audio_url,
|
||||
prefix=payload.voice_name,
|
||||
target_model="cosyvoice-v2"
|
||||
)
|
||||
|
||||
return success_response(
|
||||
VoiceCloneResponse(
|
||||
voice_id=voice_id,
|
||||
status="PENDING",
|
||||
message="音色克隆任务已创建,请稍后查询状态"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"克隆失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/voices/clone/{voice_id}/status", response_model=ApiResponse[VoiceCloneStatusResponse])
|
||||
def get_clone_status(
|
||||
voice_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: AuthedUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
查询克隆音色的状态
|
||||
"""
|
||||
from ..cosyvoice_clone import query_voice
|
||||
|
||||
try:
|
||||
info = query_voice(voice_id)
|
||||
status = info.get("status", "UNKNOWN")
|
||||
|
||||
# 如果状态是 OK,检查是否已保存到数据库
|
||||
voice_library_id = None
|
||||
if status == "OK":
|
||||
existing = (
|
||||
db.query(VoiceLibrary)
|
||||
.filter(VoiceLibrary.voice_code == voice_id)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
voice_library_id = existing.id
|
||||
|
||||
return success_response(
|
||||
VoiceCloneStatusResponse(
|
||||
voice_id=voice_id,
|
||||
status=status,
|
||||
voice_library_id=voice_library_id
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/voices/clone/{voice_id}/save", response_model=ApiResponse[dict])
|
||||
def save_cloned_voice(
|
||||
voice_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: AuthedUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
将克隆成功的音色保存到音色库
|
||||
"""
|
||||
from ..cosyvoice_clone import query_voice
|
||||
|
||||
try:
|
||||
# 查询音色状态
|
||||
info = query_voice(voice_id)
|
||||
status = info.get("status")
|
||||
|
||||
if status != "OK":
|
||||
raise HTTPException(status_code=400, detail=f"音色状态为 {status},无法保存")
|
||||
|
||||
# 检查是否已存在
|
||||
existing = (
|
||||
db.query(VoiceLibrary)
|
||||
.filter(VoiceLibrary.voice_code == voice_id)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
return success_response({"voice_library_id": existing.id, "message": "音色已存在"})
|
||||
|
||||
# 获取音色信息
|
||||
voice_name = info.get("name", "克隆音色")
|
||||
|
||||
# 获取用户的恋人信息以确定性别
|
||||
lover = db.query(Lover).filter(Lover.user_id == user.id).first()
|
||||
gender = lover.gender if lover else "female"
|
||||
|
||||
# 保存到数据库
|
||||
new_voice = VoiceLibrary(
|
||||
name=voice_name,
|
||||
gender=gender,
|
||||
style_tag="克隆音色",
|
||||
avatar_url=None,
|
||||
sample_audio_url=None,
|
||||
tts_model_id="cosyvoice-v2",
|
||||
is_default=False,
|
||||
voice_code=voice_id,
|
||||
is_owned=True,
|
||||
price_gold=0
|
||||
)
|
||||
db.add(new_voice)
|
||||
db.flush()
|
||||
|
||||
return success_response({
|
||||
"voice_library_id": new_voice.id,
|
||||
"message": "音色保存成功"
|
||||
})
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"保存失败: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -994,8 +994,9 @@
|
|||
});
|
||||
},
|
||||
back() {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
// 跳转到主页(使用 reLaunch 清空页面栈)
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
});
|
||||
},
|
||||
setUp() {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@
|
|||
<view class="list_head fa sb">
|
||||
<image src="/static/images/timbre_logo.png" mode="widthFix"></image>{{ voicesInfo }}
|
||||
</view>
|
||||
|
||||
<!-- 克隆音色按钮 -->
|
||||
<view class="clone-voice-btn" @click="showCloneModal">
|
||||
<text>🎤 克隆我的音色</text>
|
||||
</view>
|
||||
|
||||
<view class="list_content">
|
||||
<view class="list_module fa sb" v-for="(item, index) in configVoicesList.voices" :key="index" v-show="item.price_gold == 0"
|
||||
@click="selectTimbre(index)">
|
||||
|
|
@ -37,6 +43,42 @@
|
|||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 克隆音色弹窗 -->
|
||||
<view class="clone-modal" v-if="cloneModalVisible" @click="cloneModalVisible = false">
|
||||
<view class="clone-content" @click.stop>
|
||||
<view class="clone-title">克隆音色</view>
|
||||
<view class="clone-desc">上传一段清晰的音频(至少3秒),AI将克隆您的音色</view>
|
||||
|
||||
<input
|
||||
v-model="cloneVoiceName"
|
||||
class="clone-input"
|
||||
placeholder="请输入音色名称(最多10个字符)"
|
||||
maxlength="10">
|
||||
</input>
|
||||
|
||||
<view class="clone-upload-area" @click="chooseAudio">
|
||||
<view v-if="!audioFile" class="upload-placeholder">
|
||||
<text>📁 点击选择音频文件</text>
|
||||
<text class="upload-tip">支持 mp3、wav 格式</text>
|
||||
</view>
|
||||
<view v-else class="upload-success">
|
||||
<text>✅ {{ audioFile.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="clone-status" v-if="cloneStatus">
|
||||
<text :class="cloneStatusClass">{{ cloneStatusText }}</text>
|
||||
</view>
|
||||
|
||||
<view class="clone-buttons">
|
||||
<view class="clone-btn cancel" @click="cancelClone">取消</view>
|
||||
<view class="clone-btn confirm" @click="startClone" :class="{ disabled: cloning }">
|
||||
{{ cloning ? '克隆中...' : '开始克隆' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
|
@ -64,9 +106,18 @@ export default {
|
|||
voice_id: '',
|
||||
},
|
||||
selectedItemIndex: -1, // 默认不选择任何项
|
||||
configVoicesList: [],
|
||||
configVoicesList: { voices: [] }, // 修复:初始化为对象
|
||||
voicesInfo: '',
|
||||
currentAudioContext: null, // 添加当前音频上下文实例
|
||||
// 克隆音色相关
|
||||
cloneModalVisible: false,
|
||||
cloneVoiceName: '',
|
||||
audioFile: null,
|
||||
audioUrl: '',
|
||||
cloning: false,
|
||||
cloneStatus: '',
|
||||
cloneVoiceId: '',
|
||||
baseURLPy: 'http://127.0.0.1:8000',
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
|
|
@ -215,6 +266,247 @@ export default {
|
|||
console.log(this.form)
|
||||
this.loverVoice()
|
||||
}
|
||||
},
|
||||
|
||||
// ===== 克隆音色相关方法 =====
|
||||
showCloneModal() {
|
||||
this.cloneModalVisible = true;
|
||||
this.cloneVoiceName = '';
|
||||
this.audioFile = null;
|
||||
this.audioUrl = '';
|
||||
this.cloneStatus = '';
|
||||
},
|
||||
|
||||
cancelClone() {
|
||||
this.cloneModalVisible = false;
|
||||
this.cloning = false;
|
||||
},
|
||||
|
||||
chooseAudio() {
|
||||
uni.chooseFile({
|
||||
count: 1,
|
||||
extension: ['.mp3', '.wav', '.m4a'],
|
||||
success: (res) => {
|
||||
this.audioFile = {
|
||||
path: res.tempFilePaths[0],
|
||||
name: res.tempFiles[0].name || '音频文件'
|
||||
};
|
||||
console.log('选择的音频:', this.audioFile);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('选择文件失败:', err);
|
||||
uni.showToast({
|
||||
title: '选择文件失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async startClone() {
|
||||
if (this.cloning) return;
|
||||
|
||||
if (!this.cloneVoiceName.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入音色名称',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.audioFile) {
|
||||
uni.showToast({
|
||||
title: '请选择音频文件',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.cloning = true;
|
||||
this.cloneStatus = 'uploading';
|
||||
|
||||
try {
|
||||
// 1. 上传音频文件到 OSS
|
||||
const uploadResult = await this.uploadAudio();
|
||||
if (!uploadResult) {
|
||||
throw new Error('上传失败');
|
||||
}
|
||||
|
||||
this.audioUrl = uploadResult;
|
||||
this.cloneStatus = 'cloning';
|
||||
|
||||
// 2. 调用克隆 API
|
||||
const cloneResult = await this.callCloneAPI();
|
||||
if (!cloneResult) {
|
||||
throw new Error('克隆失败');
|
||||
}
|
||||
|
||||
this.cloneVoiceId = cloneResult.voice_id;
|
||||
this.cloneStatus = 'polling';
|
||||
|
||||
// 3. 轮询状态
|
||||
await this.pollCloneStatus();
|
||||
|
||||
} catch (error) {
|
||||
console.error('克隆失败:', error);
|
||||
this.cloneStatus = 'failed';
|
||||
this.cloning = false;
|
||||
}
|
||||
},
|
||||
|
||||
uploadAudio() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: this.baseURL + '/api/common/upload',
|
||||
filePath: this.audioFile.path,
|
||||
name: 'file',
|
||||
header: {
|
||||
token: uni.getStorageSync("token") || "",
|
||||
},
|
||||
success: (res) => {
|
||||
const data = JSON.parse(res.data);
|
||||
if (data.code === 1) {
|
||||
resolve(data.data.url);
|
||||
} else {
|
||||
reject(new Error(data.msg));
|
||||
}
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
callCloneAPI() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: this.baseURLPy + '/config/voices/clone',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'token': uni.getStorageSync("token") || "",
|
||||
},
|
||||
data: {
|
||||
audio_url: this.audioUrl,
|
||||
voice_name: this.cloneVoiceName,
|
||||
gender: this.form.gender || 'female'
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.data.code === 1) {
|
||||
resolve(res.data.data);
|
||||
} else {
|
||||
reject(new Error(res.data.message));
|
||||
}
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async pollCloneStatus() {
|
||||
const maxAttempts = 30; // 最多轮询30次
|
||||
const interval = 10000; // 每10秒轮询一次
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
|
||||
try {
|
||||
const status = await this.checkCloneStatus();
|
||||
|
||||
if (status === 'OK') {
|
||||
// 克隆成功,保存到数据库
|
||||
await this.saveClonedVoice();
|
||||
this.cloneStatus = 'success';
|
||||
this.cloning = false;
|
||||
|
||||
uni.showToast({
|
||||
title: '克隆成功!',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 刷新音色列表
|
||||
setTimeout(() => {
|
||||
this.cloneModalVisible = false;
|
||||
this.configVoices();
|
||||
}, 2000);
|
||||
|
||||
return;
|
||||
} else if (status === 'FAILED') {
|
||||
throw new Error('克隆失败');
|
||||
}
|
||||
// 继续轮询
|
||||
} catch (error) {
|
||||
console.error('查询状态失败:', error);
|
||||
this.cloneStatus = 'failed';
|
||||
this.cloning = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 超时
|
||||
this.cloneStatus = 'timeout';
|
||||
this.cloning = false;
|
||||
},
|
||||
|
||||
checkCloneStatus() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: this.baseURLPy + `/config/voices/clone/${this.cloneVoiceId}/status`,
|
||||
method: 'GET',
|
||||
header: {
|
||||
'token': uni.getStorageSync("token") || "",
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.data.code === 1) {
|
||||
resolve(res.data.data.status);
|
||||
} else {
|
||||
reject(new Error(res.data.message));
|
||||
}
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
saveClonedVoice() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: this.baseURLPy + `/config/voices/clone/${this.cloneVoiceId}/save`,
|
||||
method: 'POST',
|
||||
header: {
|
||||
'token': uni.getStorageSync("token") || "",
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.data.code === 1) {
|
||||
resolve(res.data.data);
|
||||
} else {
|
||||
reject(new Error(res.data.message));
|
||||
}
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
cloneStatusText() {
|
||||
const statusMap = {
|
||||
'uploading': '正在上传音频...',
|
||||
'cloning': '正在克隆音色...',
|
||||
'polling': '正在生成音色,请稍候...',
|
||||
'success': '✅ 克隆成功!',
|
||||
'failed': '❌ 克隆失败',
|
||||
'timeout': '⏱️ 克隆超时,请稍后重试'
|
||||
};
|
||||
return statusMap[this.cloneStatus] || '';
|
||||
},
|
||||
|
||||
cloneStatusClass() {
|
||||
return {
|
||||
'status-processing': ['uploading', 'cloning', 'polling'].includes(this.cloneStatus),
|
||||
'status-success': this.cloneStatus === 'success',
|
||||
'status-error': ['failed', 'timeout'].includes(this.cloneStatus)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -353,4 +645,145 @@ page {
|
|||
background: linear-gradient(135deg, #9F47FF 0%, #0053FA 100%);
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
/* 克隆音色按钮 */
|
||||
.clone-voice-btn {
|
||||
margin: 0 0 30rpx 0;
|
||||
padding: 24rpx 40rpx;
|
||||
background: linear-gradient(135deg, #FF6B9D 0%, #C239B3 100%);
|
||||
border-radius: 12rpx;
|
||||
text-align: center;
|
||||
color: #FFFFFF;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 克隆音色弹窗 */
|
||||
.clone-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.clone-content {
|
||||
width: 85%;
|
||||
max-width: 600rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 20rpx;
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
.clone-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.clone-desc {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
margin-bottom: 30rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.clone-input {
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
border: 2rpx solid #E0E0E0;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.clone-upload-area {
|
||||
width: 100%;
|
||||
min-height: 200rpx;
|
||||
border: 2rpx dashed #9F47FF;
|
||||
border-radius: 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 30rpx;
|
||||
background: #F9F9F9;
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.upload-placeholder text:first-child {
|
||||
font-size: 32rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.upload-success {
|
||||
color: #4CAF50;
|
||||
font-size: 28rpx;
|
||||
padding: 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.clone-status {
|
||||
text-align: center;
|
||||
margin-bottom: 30rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
.clone-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.clone-btn {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
text-align: center;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.clone-btn.cancel {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.clone-btn.confirm {
|
||||
background: linear-gradient(135deg, #9F47FF 0%, #0053FA 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.clone-btn.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -74,7 +74,31 @@
|
|||
title: '图片加载中...'
|
||||
});
|
||||
|
||||
// 同时下载两个图片
|
||||
// #ifdef H5
|
||||
// H5 环境直接使用图片 URL,不下载
|
||||
const topImageUrl = 'https://nvlovers.oss-cn-qingdao.aliyuncs.com/uploads/20251226/cb61a8209c59166e5a56e9c5c470e8f1.png';
|
||||
const qrCodeUrl = 'https://nvlovers.oss-cn-qingdao.aliyuncs.com/uploads/20251226/54a0da02080e49dbf2d6412791c58281.png';
|
||||
|
||||
this.topImageLocalPath = topImageUrl;
|
||||
this.qrCodeLocalPath = qrCodeUrl;
|
||||
|
||||
// 获取顶部图片信息
|
||||
uni.getImageInfo({
|
||||
src: topImageUrl,
|
||||
success: (res) => {
|
||||
this.topImageRatio = res.width / res.height;
|
||||
resolve();
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('获取图片信息失败:', err);
|
||||
this.topImageRatio = 3;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
// 非 H5 环境下载图片
|
||||
Promise.all([
|
||||
this.downloadImage('https://nvlovers.oss-cn-qingdao.aliyuncs.com/uploads/20251226/cb61a8209c59166e5a56e9c5c470e8f1.png'),
|
||||
this.downloadImage('https://nvlovers.oss-cn-qingdao.aliyuncs.com/uploads/20251226/54a0da02080e49dbf2d6412791c58281.png')
|
||||
|
|
@ -86,14 +110,12 @@
|
|||
uni.getImageInfo({
|
||||
src: topImagePath,
|
||||
success: (res) => {
|
||||
// 计算图片的宽高比
|
||||
this.topImageRatio = res.width / res.height;
|
||||
resolve();
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('获取图片信息失败:', err);
|
||||
// 如果获取失败,使用默认比例
|
||||
this.topImageRatio = 3; // 默认宽高比 3:1
|
||||
this.topImageRatio = 3;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
|
@ -101,6 +123,7 @@
|
|||
console.error('下载图片失败:', err);
|
||||
reject(err);
|
||||
});
|
||||
// #endif
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,8 +1,9 @@
|
|||
1. 将密码校验删除,因为无法生成模型,用最简单的方法来尝试这些内容。
|
||||
2. 将Hbuilder的AppId更换成自己的,原本的保留(__UNI__1F3C178)。还是无法正常编译,将下面的插件注释掉不用,Agora-RTC:音视频插件和AudioRecode:录音插件。
|
||||
- [ ] 增加tab栏但是还没有加上对应的功能
|
||||
3. 增加聊天背景选择功能,会员可以自定义背景
|
||||
4. 增加恋人消息编辑功能,更新**数据库**,加上一些编辑消息、编辑时间相关字段。用户编辑消息之后恋人不回答,只会更新记忆和摘要的数据库,下一次回答的时候会重新引用这个更新后的记忆。
|
||||
- [ ] 礼物、换装、音色样式更改,但是还未更新数据库
|
||||
5. 恋人消息回复增加思考中...
|
||||
6.
|
||||
- [ ] 恋人消息回复和消息编辑都需要测试
|
||||
6. 将Hbuilder的AppId更换成自己的,原本的保留(__UNI__1F3C178)。还是无法正常编译,将下面的插件注释掉不用,Agora-RTC:音视频插件和AudioRecode:录音插件。
|
||||
- [ ] 克隆音色API填写然后给你测试
|
||||
Loading…
Reference in New Issue
Block a user