功能:可以克隆用户的音色进行使用
This commit is contained in:
parent
451906a9ac
commit
774b12516f
Binary file not shown.
|
|
@ -328,3 +328,167 @@ def list_available_voices_for_lover(
|
||||||
),
|
),
|
||||||
msg="获取可用音色成功",
|
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() {
|
back() {
|
||||||
uni.navigateBack({
|
// 跳转到主页(使用 reLaunch 清空页面栈)
|
||||||
delta: 1,
|
uni.reLaunch({
|
||||||
|
url: '/pages/index/index'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setUp() {
|
setUp() {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,12 @@
|
||||||
<view class="list_head fa sb">
|
<view class="list_head fa sb">
|
||||||
<image src="/static/images/timbre_logo.png" mode="widthFix"></image>{{ voicesInfo }}
|
<image src="/static/images/timbre_logo.png" mode="widthFix"></image>{{ voicesInfo }}
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 克隆音色按钮 -->
|
||||||
|
<view class="clone-voice-btn" @click="showCloneModal">
|
||||||
|
<text>🎤 克隆我的音色</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="list_content">
|
<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"
|
<view class="list_module fa sb" v-for="(item, index) in configVoicesList.voices" :key="index" v-show="item.price_gold == 0"
|
||||||
@click="selectTimbre(index)">
|
@click="selectTimbre(index)">
|
||||||
|
|
@ -37,6 +43,42 @@
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</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>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -64,9 +106,18 @@ export default {
|
||||||
voice_id: '',
|
voice_id: '',
|
||||||
},
|
},
|
||||||
selectedItemIndex: -1, // 默认不选择任何项
|
selectedItemIndex: -1, // 默认不选择任何项
|
||||||
configVoicesList: [],
|
configVoicesList: { voices: [] }, // 修复:初始化为对象
|
||||||
voicesInfo: '',
|
voicesInfo: '',
|
||||||
currentAudioContext: null, // 添加当前音频上下文实例
|
currentAudioContext: null, // 添加当前音频上下文实例
|
||||||
|
// 克隆音色相关
|
||||||
|
cloneModalVisible: false,
|
||||||
|
cloneVoiceName: '',
|
||||||
|
audioFile: null,
|
||||||
|
audioUrl: '',
|
||||||
|
cloning: false,
|
||||||
|
cloneStatus: '',
|
||||||
|
cloneVoiceId: '',
|
||||||
|
baseURLPy: 'http://127.0.0.1:8000',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoad(options) {
|
onLoad(options) {
|
||||||
|
|
@ -215,6 +266,247 @@ export default {
|
||||||
console.log(this.form)
|
console.log(this.form)
|
||||||
this.loverVoice()
|
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%);
|
background: linear-gradient(135deg, #9F47FF 0%, #0053FA 100%);
|
||||||
border-radius: 12rpx;
|
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>
|
</style>
|
||||||
|
|
@ -74,7 +74,31 @@
|
||||||
title: '图片加载中...'
|
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([
|
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/cb61a8209c59166e5a56e9c5c470e8f1.png'),
|
||||||
this.downloadImage('https://nvlovers.oss-cn-qingdao.aliyuncs.com/uploads/20251226/54a0da02080e49dbf2d6412791c58281.png')
|
this.downloadImage('https://nvlovers.oss-cn-qingdao.aliyuncs.com/uploads/20251226/54a0da02080e49dbf2d6412791c58281.png')
|
||||||
|
|
@ -86,14 +110,12 @@
|
||||||
uni.getImageInfo({
|
uni.getImageInfo({
|
||||||
src: topImagePath,
|
src: topImagePath,
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
// 计算图片的宽高比
|
|
||||||
this.topImageRatio = res.width / res.height;
|
this.topImageRatio = res.width / res.height;
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
console.error('获取图片信息失败:', err);
|
console.error('获取图片信息失败:', err);
|
||||||
// 如果获取失败,使用默认比例
|
this.topImageRatio = 3;
|
||||||
this.topImageRatio = 3; // 默认宽高比 3:1
|
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -101,6 +123,7 @@
|
||||||
console.error('下载图片失败:', err);
|
console.error('下载图片失败:', err);
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
// #endif
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,8 +1,9 @@
|
||||||
1. 将密码校验删除,因为无法生成模型,用最简单的方法来尝试这些内容。
|
1. 将密码校验删除,因为无法生成模型,用最简单的方法来尝试这些内容。
|
||||||
2. 将Hbuilder的AppId更换成自己的,原本的保留(__UNI__1F3C178)。还是无法正常编译,将下面的插件注释掉不用,Agora-RTC:音视频插件和AudioRecode:录音插件。
|
|
||||||
- [ ] 增加tab栏但是还没有加上对应的功能
|
- [ ] 增加tab栏但是还没有加上对应的功能
|
||||||
3. 增加聊天背景选择功能,会员可以自定义背景
|
3. 增加聊天背景选择功能,会员可以自定义背景
|
||||||
4. 增加恋人消息编辑功能,更新**数据库**,加上一些编辑消息、编辑时间相关字段。用户编辑消息之后恋人不回答,只会更新记忆和摘要的数据库,下一次回答的时候会重新引用这个更新后的记忆。
|
4. 增加恋人消息编辑功能,更新**数据库**,加上一些编辑消息、编辑时间相关字段。用户编辑消息之后恋人不回答,只会更新记忆和摘要的数据库,下一次回答的时候会重新引用这个更新后的记忆。
|
||||||
- [ ] 礼物、换装、音色样式更改,但是还未更新数据库
|
- [ ] 礼物、换装、音色样式更改,但是还未更新数据库
|
||||||
5. 恋人消息回复增加思考中...
|
5. 恋人消息回复增加思考中...
|
||||||
6.
|
- [ ] 恋人消息回复和消息编辑都需要测试
|
||||||
|
6. 将Hbuilder的AppId更换成自己的,原本的保留(__UNI__1F3C178)。还是无法正常编译,将下面的插件注释掉不用,Agora-RTC:音视频插件和AudioRecode:录音插件。
|
||||||
|
- [ ] 克隆音色API填写然后给你测试
|
||||||
Loading…
Reference in New Issue
Block a user