测试通过:
- 自定义恋人历史消息 - 克隆音色 - 回复消息增加“思考中”
This commit is contained in:
parent
3a89cddd43
commit
b4f4800e77
4
.env
4
.env
|
|
@ -2,6 +2,10 @@
|
||||||
APP_ENV=development
|
APP_ENV=development
|
||||||
DEBUG=True
|
DEBUG=True
|
||||||
|
|
||||||
|
# ===== 后端服务地址 =====
|
||||||
|
# 用于生成完整的 URL(TTS 音频等)
|
||||||
|
BACKEND_URL=http://127.0.0.1:8000
|
||||||
|
|
||||||
# ===== 数据库配置 =====
|
# ===== 数据库配置 =====
|
||||||
DATABASE_URL=mysql+pymysql://root:root@127.0.0.1:3306/lover?charset=utf8mb4
|
DATABASE_URL=mysql+pymysql://root:root@127.0.0.1:3306/lover?charset=utf8mb4
|
||||||
|
|
||||||
|
|
|
||||||
BIN
lover/__pycache__/cosyvoice_clone.cpython-314.pyc
Normal file
BIN
lover/__pycache__/cosyvoice_clone.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
|
|
@ -1,8 +1,10 @@
|
||||||
from fastapi import FastAPI, HTTPException, Request
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
import logging
|
import logging
|
||||||
import dashscope
|
import dashscope
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from .routers import config as config_router
|
from .routers import config as config_router
|
||||||
from .routers import lover as lover_router
|
from .routers import lover as lover_router
|
||||||
|
|
@ -22,6 +24,13 @@ if settings.DASHSCOPE_API_KEY:
|
||||||
|
|
||||||
app = FastAPI(title="LOVER API")
|
app = FastAPI(title="LOVER API")
|
||||||
|
|
||||||
|
# 创建 TTS 文件目录
|
||||||
|
tts_dir = Path("public/tts")
|
||||||
|
tts_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 挂载静态文件服务(用于提供 TTS 音频文件)
|
||||||
|
app.mount("/tts", StaticFiles(directory=str(tts_dir)), name="tts")
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=[
|
allow_origins=[
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -475,19 +475,56 @@ def _pick_available_voice(db: Session, lover: Lover, user_row: User) -> VoiceLib
|
||||||
|
|
||||||
|
|
||||||
def _upload_tts_to_oss(file_bytes: bytes, lover_id: int, message_id: int) -> str:
|
def _upload_tts_to_oss(file_bytes: bytes, lover_id: int, message_id: int) -> str:
|
||||||
if not settings.ALIYUN_OSS_ACCESS_KEY_ID or not settings.ALIYUN_OSS_ACCESS_KEY_SECRET:
|
"""
|
||||||
raise HTTPException(status_code=500, detail="未配置 OSS Key")
|
上传 TTS 音频文件。
|
||||||
if not settings.ALIYUN_OSS_BUCKET_NAME or not settings.ALIYUN_OSS_ENDPOINT:
|
优先使用 OSS,如果未配置则保存到本地。
|
||||||
raise HTTPException(status_code=500, detail="未配置 OSS Bucket/Endpoint")
|
"""
|
||||||
|
# 检查是否配置了 OSS
|
||||||
|
has_oss = (
|
||||||
|
settings.ALIYUN_OSS_ACCESS_KEY_ID
|
||||||
|
and settings.ALIYUN_OSS_ACCESS_KEY_SECRET
|
||||||
|
and settings.ALIYUN_OSS_BUCKET_NAME
|
||||||
|
and settings.ALIYUN_OSS_ENDPOINT
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_oss:
|
||||||
|
# 使用 OSS 上传
|
||||||
object_name = f"lover/{lover_id}/tts/{message_id}.mp3"
|
object_name = f"lover/{lover_id}/tts/{message_id}.mp3"
|
||||||
endpoint = settings.ALIYUN_OSS_ENDPOINT.rstrip("/")
|
endpoint = settings.ALIYUN_OSS_ENDPOINT.rstrip("/")
|
||||||
try:
|
try:
|
||||||
auth = oss2.Auth(settings.ALIYUN_OSS_ACCESS_KEY_ID, settings.ALIYUN_OSS_ACCESS_KEY_SECRET)
|
auth = oss2.Auth(settings.ALIYUN_OSS_ACCESS_KEY_ID, settings.ALIYUN_OSS_ACCESS_KEY_SECRET)
|
||||||
bucket = oss2.Bucket(auth, endpoint, settings.ALIYUN_OSS_BUCKET_NAME)
|
bucket = oss2.Bucket(auth, endpoint, settings.ALIYUN_OSS_BUCKET_NAME)
|
||||||
bucket.put_object(object_name, file_bytes)
|
bucket.put_object(object_name, file_bytes)
|
||||||
|
|
||||||
|
# 返回 OSS URL
|
||||||
|
cdn_domain = settings.ALIYUN_OSS_CDN_DOMAIN
|
||||||
|
if cdn_domain:
|
||||||
|
return f"{cdn_domain.rstrip('/')}/{object_name}"
|
||||||
|
else:
|
||||||
|
return f"https://{settings.ALIYUN_OSS_BUCKET_NAME}.{endpoint}/{object_name}"
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(status_code=502, detail=f"上传语音失败: {exc}") from exc
|
raise HTTPException(status_code=502, detail=f"上传语音失败: {exc}") from exc
|
||||||
|
else:
|
||||||
|
# 保存到本地文件系统
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 创建保存目录
|
||||||
|
base_dir = Path("public/tts")
|
||||||
|
lover_dir = base_dir / str(lover_id)
|
||||||
|
lover_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 保存文件
|
||||||
|
file_path = lover_dir / f"{message_id}.mp3"
|
||||||
|
try:
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
f.write(file_bytes)
|
||||||
|
|
||||||
|
# 返回完整 URL(使用环境变量配置的后端地址)
|
||||||
|
backend_url = os.getenv("BACKEND_URL", "http://127.0.0.1:8000")
|
||||||
|
return f"{backend_url.rstrip('/')}/tts/{lover_id}/{message_id}.mp3"
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(status_code=500, detail=f"保存语音文件失败: {exc}") from exc
|
||||||
|
|
||||||
cdn = settings.ALIYUN_OSS_CDN_DOMAIN
|
cdn = settings.ALIYUN_OSS_CDN_DOMAIN
|
||||||
if cdn:
|
if cdn:
|
||||||
|
|
|
||||||
|
|
@ -334,7 +334,7 @@ def list_available_voices_for_lover(
|
||||||
|
|
||||||
class VoiceCloneRequest(BaseModel):
|
class VoiceCloneRequest(BaseModel):
|
||||||
audio_url: str
|
audio_url: str
|
||||||
voice_name: str
|
voice_name: str # 用户输入的显示名称(可以是中文)
|
||||||
gender: str # male/female
|
gender: str # male/female
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
@ -364,25 +364,38 @@ def clone_voice(
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
克隆音色:用户上传音频文件,系统调用 CosyVoice 克隆音色
|
克隆音色:用户上传音频文件,系统调用 CosyVoice 克隆音色
|
||||||
|
用户可以输入中文名称,系统会自动生成英文哈希码用于 API 调用
|
||||||
"""
|
"""
|
||||||
from ..cosyvoice_clone import create_voice_from_url
|
from ..cosyvoice_clone import create_voice_from_url
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
|
||||||
# 验证音色名称长度(CosyVoice 限制 prefix <= 10 字符)
|
# 验证音色名称长度
|
||||||
if len(payload.voice_name) > 10:
|
if len(payload.voice_name) > 20:
|
||||||
raise HTTPException(status_code=400, detail="音色名称不能超过10个字符")
|
raise HTTPException(status_code=400, detail="音色名称不能超过20个字符")
|
||||||
|
|
||||||
# 验证性别
|
# 验证性别
|
||||||
if payload.gender not in ["male", "female"]:
|
if payload.gender not in ["male", "female"]:
|
||||||
raise HTTPException(status_code=400, detail="性别必须是 male 或 female")
|
raise HTTPException(status_code=400, detail="性别必须是 male 或 female")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 调用克隆服务
|
# 生成英文哈希码作为 prefix(CosyVoice API 要求最多10字符)
|
||||||
|
# 使用时间戳 + 用户ID 生成唯一哈希
|
||||||
|
hash_input = f"{user.id}_{int(time.time())}"
|
||||||
|
hash_code = hashlib.md5(hash_input.encode()).hexdigest()[:6] # 取6位
|
||||||
|
api_prefix = f"v{hash_code}" # v + 6位哈希 = 7字符,符合限制
|
||||||
|
|
||||||
|
# 调用克隆服务(使用英文哈希码)
|
||||||
voice_id = create_voice_from_url(
|
voice_id = create_voice_from_url(
|
||||||
audio_url=payload.audio_url,
|
audio_url=payload.audio_url,
|
||||||
prefix=payload.voice_name,
|
prefix=api_prefix,
|
||||||
target_model="cosyvoice-v2"
|
target_model="cosyvoice-v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 将中文显示名称和 voice_id 的映射关系临时存储
|
||||||
|
# 后续保存到数据库时会用到
|
||||||
|
# 这里可以使用 Redis 或者直接在保存时传递
|
||||||
|
|
||||||
return success_response(
|
return success_response(
|
||||||
VoiceCloneResponse(
|
VoiceCloneResponse(
|
||||||
voice_id=voice_id,
|
voice_id=voice_id,
|
||||||
|
|
@ -431,9 +444,16 @@ def get_clone_status(
|
||||||
raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
class VoiceCloneSaveRequest(BaseModel):
|
||||||
|
display_name: Optional[str] = None # 用户输入的显示名称(中文)
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/voices/clone/{voice_id}/save", response_model=ApiResponse[dict])
|
@router.post("/voices/clone/{voice_id}/save", response_model=ApiResponse[dict])
|
||||||
def save_cloned_voice(
|
def save_cloned_voice(
|
||||||
voice_id: str,
|
voice_id: str,
|
||||||
|
payload: VoiceCloneSaveRequest,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
user: AuthedUser = Depends(get_current_user),
|
user: AuthedUser = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
|
|
@ -460,22 +480,25 @@ def save_cloned_voice(
|
||||||
return success_response({"voice_library_id": existing.id, "message": "音色已存在"})
|
return success_response({"voice_library_id": existing.id, "message": "音色已存在"})
|
||||||
|
|
||||||
# 获取音色信息
|
# 获取音色信息
|
||||||
voice_name = info.get("name", "克隆音色")
|
api_voice_name = info.get("name", "克隆音色") # API 返回的英文哈希名称
|
||||||
|
|
||||||
|
# 使用用户输入的显示名称,如果没有则使用 API 名称
|
||||||
|
display_name = payload.display_name if payload.display_name else api_voice_name
|
||||||
|
|
||||||
# 获取用户的恋人信息以确定性别
|
# 获取用户的恋人信息以确定性别
|
||||||
lover = db.query(Lover).filter(Lover.user_id == user.id).first()
|
lover = db.query(Lover).filter(Lover.user_id == user.id).first()
|
||||||
gender = lover.gender if lover else "female"
|
gender = lover.gender if lover else "female"
|
||||||
|
|
||||||
# 保存到数据库
|
# 保存到数据库(name 字段保存用户输入的中文名称)
|
||||||
new_voice = VoiceLibrary(
|
new_voice = VoiceLibrary(
|
||||||
name=voice_name,
|
name=display_name, # 保存中文显示名称
|
||||||
gender=gender,
|
gender=gender,
|
||||||
style_tag="克隆音色",
|
style_tag="克隆音色",
|
||||||
avatar_url=None,
|
avatar_url=None,
|
||||||
sample_audio_url=None,
|
sample_audio_url=None,
|
||||||
tts_model_id="cosyvoice-v2",
|
tts_model_id="cosyvoice-v2",
|
||||||
is_default=False,
|
is_default=False,
|
||||||
voice_code=voice_id,
|
voice_code=voice_id, # voice_code 是 API 返回的英文哈希码
|
||||||
is_owned=True,
|
is_owned=True,
|
||||||
price_gold=0
|
price_gold=0
|
||||||
)
|
)
|
||||||
|
|
@ -484,7 +507,8 @@ def save_cloned_voice(
|
||||||
|
|
||||||
return success_response({
|
return success_response({
|
||||||
"voice_library_id": new_voice.id,
|
"voice_library_id": new_voice.id,
|
||||||
"message": "音色保存成功"
|
"message": "音色保存成功",
|
||||||
|
"display_name": display_name
|
||||||
})
|
})
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,7 @@ class LoverBasicResponse(BaseModel):
|
||||||
lover_id: int
|
lover_id: int
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
image_url: Optional[str] = None
|
image_url: Optional[str] = None
|
||||||
|
gender: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
LoverConfigResponse.model_rebuild()
|
LoverConfigResponse.model_rebuild()
|
||||||
|
|
@ -1052,7 +1053,12 @@ def get_lover_basic(
|
||||||
# 保持成功响应结构,data 返回 null 便于前端按需处理
|
# 保持成功响应结构,data 返回 null 便于前端按需处理
|
||||||
return success_response(None)
|
return success_response(None)
|
||||||
|
|
||||||
return success_response(LoverBasicResponse(lover_id=lover.id, name=lover.name, image_url=lover.image_url))
|
return success_response(LoverBasicResponse(
|
||||||
|
lover_id=lover.id,
|
||||||
|
name=lover.name,
|
||||||
|
image_url=lover.image_url,
|
||||||
|
gender=lover.gender
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/config", response_model=ApiResponse[LoverOut])
|
@router.post("/config", response_model=ApiResponse[LoverOut])
|
||||||
|
|
|
||||||
|
|
@ -546,11 +546,11 @@
|
||||||
|
|
||||||
// 延迟刷新,确保至少显示5秒思考中
|
// 延迟刷新,确保至少显示5秒思考中
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.showToast({
|
// 移除临时消息和思考中消息
|
||||||
title: '发送成功',
|
this.sessionInitList.messages = this.sessionInitList.messages.filter(
|
||||||
icon: 'none',
|
msg => !msg.isTemp && !msg.isThinking
|
||||||
position: 'top'
|
);
|
||||||
});
|
|
||||||
this.addBond();
|
this.addBond();
|
||||||
this.form.message = '';
|
this.form.message = '';
|
||||||
this.form.session_id = '';
|
this.form.session_id = '';
|
||||||
|
|
@ -561,9 +561,9 @@
|
||||||
this.refreshSessionData(true);
|
this.refreshSessionData(true);
|
||||||
}, remainingTime);
|
}, remainingTime);
|
||||||
} else {
|
} else {
|
||||||
// 发送失败,立即移除思考中消息
|
// 发送失败,立即移除临时消息和思考中消息
|
||||||
this.sessionInitList.messages = this.sessionInitList.messages.filter(
|
this.sessionInitList.messages = this.sessionInitList.messages.filter(
|
||||||
msg => !msg.isThinking
|
msg => !msg.isTemp && !msg.isThinking
|
||||||
);
|
);
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: res.msg,
|
title: res.msg,
|
||||||
|
|
@ -572,9 +572,9 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
// 发送失败,立即移除思考中消息
|
// 发送失败,立即移除临时消息和思考中消息
|
||||||
this.sessionInitList.messages = this.sessionInitList.messages.filter(
|
this.sessionInitList.messages = this.sessionInitList.messages.filter(
|
||||||
msg => !msg.isThinking
|
msg => !msg.isTemp && !msg.isThinking
|
||||||
);
|
);
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '发送失败',
|
title: '发送失败',
|
||||||
|
|
@ -883,33 +883,56 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录发送时间,用于计算最小思考时间
|
// 保存用户消息内容
|
||||||
this.messageSentTime = Date.now();
|
const userMessage = this.form.message;
|
||||||
|
|
||||||
// 立即添加一条"思考中"的临时消息
|
// 立即添加用户消息到界面
|
||||||
const thinkingMessage = {
|
const userMsg = {
|
||||||
id: 'thinking_' + Date.now(),
|
id: Date.now(), // 临时ID
|
||||||
role: 'lover',
|
role: 'user',
|
||||||
content: '思考中...',
|
content: userMessage,
|
||||||
isThinking: true, // 标记为思考中状态
|
seq: this.sessionInitList.messages.length + 1,
|
||||||
created_at: new Date().toISOString()
|
created_at: new Date().toISOString(),
|
||||||
|
content_type: 'text',
|
||||||
|
isTemp: true // 标记为临时消息
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加到消息列表
|
// 添加到消息列表
|
||||||
if (!this.sessionInitList.messages) {
|
if (!this.sessionInitList.messages) {
|
||||||
this.sessionInitList.messages = [];
|
this.sessionInitList.messages = [];
|
||||||
}
|
}
|
||||||
this.sessionInitList.messages.push(thinkingMessage);
|
this.sessionInitList.messages.push(userMsg);
|
||||||
|
|
||||||
|
// 立即添加"思考中..."消息
|
||||||
|
const thinkingMsg = {
|
||||||
|
id: Date.now() + 1,
|
||||||
|
role: 'lover',
|
||||||
|
content: '思考中...',
|
||||||
|
seq: this.sessionInitList.messages.length + 1,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
content_type: 'text',
|
||||||
|
isThinking: true // 标记为思考中消息
|
||||||
|
};
|
||||||
|
this.sessionInitList.messages.push(thinkingMsg);
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
this.form.message = '';
|
||||||
|
|
||||||
// 滚动到底部
|
// 滚动到底部
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 记录发送时间
|
||||||
|
this.messageSentTime = Date.now();
|
||||||
|
|
||||||
|
// 恢复消息内容用于发送
|
||||||
|
this.form.message = userMessage;
|
||||||
|
|
||||||
this.addBondform.type = 1;
|
this.addBondform.type = 1;
|
||||||
this.addBondform.num = 1;
|
this.addBondform.num = 1;
|
||||||
|
|
||||||
// 发送消息的API调用
|
// 后台发送消息(不显示loading)
|
||||||
this.sessionSend();
|
this.sessionSend();
|
||||||
},
|
},
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
|
|
@ -1078,15 +1101,52 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存用户消息内容
|
||||||
|
const userMessage = this.form.message;
|
||||||
|
|
||||||
uni.showLoading({
|
// 立即添加用户消息到界面
|
||||||
title: '发送中...'
|
const userMsg = {
|
||||||
|
id: Date.now(), // 临时ID
|
||||||
|
role: 'user',
|
||||||
|
content: userMessage,
|
||||||
|
seq: this.sessionInitList.messages.length + 1,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
content_type: 'text',
|
||||||
|
isTemp: true // 标记为临时消息
|
||||||
|
};
|
||||||
|
this.sessionInitList.messages.push(userMsg);
|
||||||
|
|
||||||
|
// 立即添加"思考中..."消息
|
||||||
|
const thinkingMsg = {
|
||||||
|
id: Date.now() + 1,
|
||||||
|
role: 'lover',
|
||||||
|
content: '思考中...',
|
||||||
|
seq: this.sessionInitList.messages.length + 1,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
content_type: 'text',
|
||||||
|
isThinking: true // 标记为思考中消息
|
||||||
|
};
|
||||||
|
this.sessionInitList.messages.push(thinkingMsg);
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
this.form.message = '';
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.scrollToBottom();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 记录发送时间
|
||||||
|
this.messageSentTime = Date.now();
|
||||||
|
|
||||||
|
// 恢复消息内容用于发送
|
||||||
|
this.form.message = userMessage;
|
||||||
|
|
||||||
|
// 设置亲密度参数
|
||||||
this.addBondform.type = 1;
|
this.addBondform.type = 1;
|
||||||
this.addBondform.num = 1;
|
this.addBondform.num = 1;
|
||||||
|
|
||||||
// 发送消息的API调用
|
// 后台发送消息(不显示loading)
|
||||||
this.sessionSend();
|
this.sessionSend();
|
||||||
},
|
},
|
||||||
playVoice(id) {
|
playVoice(id) {
|
||||||
|
|
|
||||||
|
|
@ -48,22 +48,50 @@
|
||||||
<view class="clone-modal" v-if="cloneModalVisible" @click="cloneModalVisible = false">
|
<view class="clone-modal" v-if="cloneModalVisible" @click="cloneModalVisible = false">
|
||||||
<view class="clone-content" @click.stop>
|
<view class="clone-content" @click.stop>
|
||||||
<view class="clone-title">克隆音色</view>
|
<view class="clone-title">克隆音色</view>
|
||||||
<view class="clone-desc">上传一段清晰的音频(至少3秒),AI将克隆您的音色</view>
|
<view class="clone-desc">上传一段清晰的音频(至少3秒),AI将克隆您的音色。可以使用中文名称。</view>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
v-model="cloneVoiceName"
|
v-model="cloneVoiceName"
|
||||||
class="clone-input"
|
class="clone-input"
|
||||||
placeholder="请输入音色名称(最多10个字符)"
|
placeholder="请输入音色名称(最多20个字符)"
|
||||||
maxlength="10">
|
maxlength="20">
|
||||||
</input>
|
</input>
|
||||||
|
|
||||||
<view class="clone-upload-area" @click="chooseAudio">
|
<!-- 音频输入方式切换 -->
|
||||||
|
<view class="audio-mode-switch">
|
||||||
|
<view
|
||||||
|
class="mode-btn"
|
||||||
|
:class="{ active: audioInputMode === 'url' }"
|
||||||
|
@click="audioInputMode = 'url'">
|
||||||
|
输入 URL
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="mode-btn"
|
||||||
|
:class="{ active: audioInputMode === 'file' }"
|
||||||
|
@click="audioInputMode = 'file'">
|
||||||
|
上传文件
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- URL 输入模式 -->
|
||||||
|
<view v-if="audioInputMode === 'url'" class="url-input-area">
|
||||||
|
<input
|
||||||
|
v-model="audioUrlInput"
|
||||||
|
class="clone-input"
|
||||||
|
placeholder="请输入音频 URL(http:// 或 https://)">
|
||||||
|
</input>
|
||||||
|
<view class="url-tip">示例:http://example.com/audio.mp3</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 文件上传模式 -->
|
||||||
|
<view v-else class="clone-upload-area" @click="chooseAudio">
|
||||||
<view v-if="!audioFile" class="upload-placeholder">
|
<view v-if="!audioFile" class="upload-placeholder">
|
||||||
<text>📁 点击选择音频文件</text>
|
<text>📁 点击选择音频文件</text>
|
||||||
<text class="upload-tip">支持 mp3、wav 格式</text>
|
<text class="upload-tip">支持 mp3、wav 格式,最大 10MB</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-else class="upload-success">
|
<view v-else class="upload-success">
|
||||||
<text>✅ {{ audioFile.name }}</text>
|
<text>✅ {{ audioFile.name }}</text>
|
||||||
|
<text class="file-size">{{ (audioFile.size / 1024 / 1024).toFixed(2) }} MB</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|
@ -118,15 +146,76 @@ export default {
|
||||||
cloneStatus: '',
|
cloneStatus: '',
|
||||||
cloneVoiceId: '',
|
cloneVoiceId: '',
|
||||||
baseURLPy: 'http://127.0.0.1:8000',
|
baseURLPy: 'http://127.0.0.1:8000',
|
||||||
|
// 音频输入方式:'file' 或 'url'
|
||||||
|
audioInputMode: 'url', // 默认使用 URL 输入
|
||||||
|
audioUrlInput: '', // 用户输入的音频 URL
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoad(options) {
|
onLoad(options) {
|
||||||
console.log('角色声音页面接参:',options)
|
console.log('角色声音页面接参:',options)
|
||||||
this.form.gender = options.sex
|
console.log('options.sex:', options.sex, 'options.is_edit:', options.is_edit);
|
||||||
console.log(this.form)
|
|
||||||
this.configVoices()
|
// 如果有传递性别参数,使用传递的参数
|
||||||
|
if (options.sex) {
|
||||||
|
console.log('使用传递的性别参数:', options.sex);
|
||||||
|
this.form.gender = options.sex;
|
||||||
|
this.configVoices();
|
||||||
|
}
|
||||||
|
// 如果是编辑模式且没有性别参数,从恋人信息中获取
|
||||||
|
else if (options.is_edit) {
|
||||||
|
console.log('编辑模式,调用 getLoverGender()');
|
||||||
|
this.getLoverGender();
|
||||||
|
}
|
||||||
|
// 其他情况,直接加载音色列表(使用可用音色接口)
|
||||||
|
else {
|
||||||
|
console.log('其他情况,直接加载音色列表');
|
||||||
|
this.configVoices();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('onLoad 结束时的 form:', this.form);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 获取恋人性别信息
|
||||||
|
getLoverGender() {
|
||||||
|
console.log('getLoverGender() 开始执行');
|
||||||
|
console.log('请求 URL:', this.baseURLPy + '/lover/basic');
|
||||||
|
console.log('token:', uni.getStorageSync("token"));
|
||||||
|
|
||||||
|
uni.request({
|
||||||
|
url: this.baseURLPy + '/lover/basic',
|
||||||
|
method: 'GET',
|
||||||
|
header: {
|
||||||
|
'token': uni.getStorageSync("token") || "",
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
console.log('getLoverGender() 响应:', res);
|
||||||
|
console.log('res.data:', res.data);
|
||||||
|
console.log('res.data.data:', res.data.data);
|
||||||
|
|
||||||
|
if (res.data.code === 1 && res.data.data) {
|
||||||
|
// 设置性别
|
||||||
|
const gender = res.data.data.gender || 'female';
|
||||||
|
console.log('从 API 获取的性别:', gender);
|
||||||
|
this.form.gender = gender;
|
||||||
|
console.log('设置后的 form.gender:', this.form.gender);
|
||||||
|
// 获取音色列表
|
||||||
|
this.configVoices();
|
||||||
|
} else {
|
||||||
|
console.warn('API 返回失败或数据为空,使用默认值 female');
|
||||||
|
// 默认使用 female
|
||||||
|
this.form.gender = 'female';
|
||||||
|
this.configVoices();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('getLoverGender() 请求失败:', error);
|
||||||
|
// 默认使用 female
|
||||||
|
this.form.gender = 'female';
|
||||||
|
this.configVoices();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
configVoices() {
|
configVoices() {
|
||||||
let api = ''
|
let api = ''
|
||||||
if(this.form.gender){
|
if(this.form.gender){
|
||||||
|
|
@ -274,7 +363,9 @@ export default {
|
||||||
this.cloneVoiceName = '';
|
this.cloneVoiceName = '';
|
||||||
this.audioFile = null;
|
this.audioFile = null;
|
||||||
this.audioUrl = '';
|
this.audioUrl = '';
|
||||||
|
this.audioUrlInput = '';
|
||||||
this.cloneStatus = '';
|
this.cloneStatus = '';
|
||||||
|
this.audioInputMode = 'url'; // 默认使用 URL 输入
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelClone() {
|
cancelClone() {
|
||||||
|
|
@ -287,9 +378,27 @@ export default {
|
||||||
count: 1,
|
count: 1,
|
||||||
extension: ['.mp3', '.wav', '.m4a'],
|
extension: ['.mp3', '.wav', '.m4a'],
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
|
const file = res.tempFiles[0];
|
||||||
|
const fileSize = file.size; // 字节
|
||||||
|
const fileSizeMB = (fileSize / 1024 / 1024).toFixed(2); // 转换为 MB
|
||||||
|
|
||||||
|
console.log('选择的文件:', file);
|
||||||
|
console.log('文件大小:', fileSizeMB, 'MB');
|
||||||
|
|
||||||
|
// 检查文件大小(限制 10MB)
|
||||||
|
if (fileSize > 10 * 1024 * 1024) {
|
||||||
|
uni.showToast({
|
||||||
|
title: `文件太大(${fileSizeMB}MB),请选择小于10MB的音频`,
|
||||||
|
icon: 'none',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.audioFile = {
|
this.audioFile = {
|
||||||
path: res.tempFilePaths[0],
|
path: res.tempFilePaths[0],
|
||||||
name: res.tempFiles[0].name || '音频文件'
|
name: file.name || '音频文件',
|
||||||
|
size: fileSize
|
||||||
};
|
};
|
||||||
console.log('选择的音频:', this.audioFile);
|
console.log('选择的音频:', this.audioFile);
|
||||||
},
|
},
|
||||||
|
|
@ -314,6 +423,39 @@ export default {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证长度(最多20个字符,因为是显示名称)
|
||||||
|
if (this.cloneVoiceName.length > 20) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '音色名称不能超过20个字符',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据输入方式验证
|
||||||
|
if (this.audioInputMode === 'url') {
|
||||||
|
// URL 输入模式
|
||||||
|
if (!this.audioUrlInput.trim()) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请输入音频 URL',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 URL 格式
|
||||||
|
if (!this.audioUrlInput.startsWith('http://') && !this.audioUrlInput.startsWith('https://')) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请输入有效的 HTTP/HTTPS URL',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接使用输入的 URL
|
||||||
|
this.audioUrl = this.audioUrlInput;
|
||||||
|
} else {
|
||||||
|
// 文件上传模式
|
||||||
if (!this.audioFile) {
|
if (!this.audioFile) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '请选择音频文件',
|
title: '请选择音频文件',
|
||||||
|
|
@ -321,21 +463,35 @@ export default {
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保有性别信息
|
||||||
|
if (!this.form.gender) {
|
||||||
|
console.warn('性别信息为空,使用默认值 female');
|
||||||
|
this.form.gender = 'female';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('开始克隆,当前性别:', this.form.gender);
|
||||||
|
console.log('用户输入的名称:', this.cloneVoiceName);
|
||||||
|
console.log('音频输入模式:', this.audioInputMode);
|
||||||
|
console.log('音频 URL:', this.audioUrl);
|
||||||
|
|
||||||
this.cloning = true;
|
this.cloning = true;
|
||||||
this.cloneStatus = 'uploading';
|
this.cloneStatus = 'cloning';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 上传音频文件到 OSS
|
// 如果是文件上传模式,先上传
|
||||||
|
if (this.audioInputMode === 'file') {
|
||||||
|
this.cloneStatus = 'uploading';
|
||||||
const uploadResult = await this.uploadAudio();
|
const uploadResult = await this.uploadAudio();
|
||||||
if (!uploadResult) {
|
if (!uploadResult) {
|
||||||
throw new Error('上传失败');
|
throw new Error('上传失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.audioUrl = uploadResult;
|
this.audioUrl = uploadResult;
|
||||||
this.cloneStatus = 'cloning';
|
}
|
||||||
|
|
||||||
// 2. 调用克隆 API
|
// 调用克隆 API
|
||||||
|
this.cloneStatus = 'cloning';
|
||||||
const cloneResult = await this.callCloneAPI();
|
const cloneResult = await this.callCloneAPI();
|
||||||
if (!cloneResult) {
|
if (!cloneResult) {
|
||||||
throw new Error('克隆失败');
|
throw new Error('克隆失败');
|
||||||
|
|
@ -344,18 +500,38 @@ export default {
|
||||||
this.cloneVoiceId = cloneResult.voice_id;
|
this.cloneVoiceId = cloneResult.voice_id;
|
||||||
this.cloneStatus = 'polling';
|
this.cloneStatus = 'polling';
|
||||||
|
|
||||||
// 3. 轮询状态
|
// 轮询状态
|
||||||
await this.pollCloneStatus();
|
await this.pollCloneStatus();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('克隆失败:', error);
|
console.error('克隆失败:', error);
|
||||||
this.cloneStatus = 'failed';
|
this.cloneStatus = 'failed';
|
||||||
this.cloning = false;
|
this.cloning = false;
|
||||||
|
|
||||||
|
// 显示更友好的错误提示
|
||||||
|
let errorMsg = '克隆失败';
|
||||||
|
if (error.message) {
|
||||||
|
if (error.message.includes('上传失败')) {
|
||||||
|
errorMsg = '音频上传失败,请重试';
|
||||||
|
} else {
|
||||||
|
errorMsg = error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: errorMsg,
|
||||||
|
icon: 'none',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadAudio() {
|
uploadAudio() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
console.log('开始上传音频...');
|
||||||
|
console.log('上传 URL:', this.baseURL + '/api/common/upload');
|
||||||
|
console.log('音频文件:', this.audioFile);
|
||||||
|
|
||||||
uni.uploadFile({
|
uni.uploadFile({
|
||||||
url: this.baseURL + '/api/common/upload',
|
url: this.baseURL + '/api/common/upload',
|
||||||
filePath: this.audioFile.path,
|
filePath: this.audioFile.path,
|
||||||
|
|
@ -364,20 +540,53 @@ export default {
|
||||||
token: uni.getStorageSync("token") || "",
|
token: uni.getStorageSync("token") || "",
|
||||||
},
|
},
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
|
console.log('上传响应:', res);
|
||||||
const data = JSON.parse(res.data);
|
const data = JSON.parse(res.data);
|
||||||
|
console.log('解析后的数据:', data);
|
||||||
|
|
||||||
if (data.code === 1) {
|
if (data.code === 1) {
|
||||||
resolve(data.data.url);
|
let audioUrl = data.data.url;
|
||||||
|
|
||||||
|
// 如果返回的是相对路径,转换为完整 URL
|
||||||
|
if (!audioUrl.startsWith('http://') && !audioUrl.startsWith('https://')) {
|
||||||
|
// 拼接完整 URL
|
||||||
|
audioUrl = this.baseURL + audioUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('上传成功,音频 URL:', audioUrl);
|
||||||
|
resolve(audioUrl);
|
||||||
} else {
|
} else {
|
||||||
|
console.error('上传失败:', data.msg);
|
||||||
reject(new Error(data.msg));
|
reject(new Error(data.msg));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fail: reject
|
fail: (error) => {
|
||||||
|
console.error('上传请求失败:', error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
callCloneAPI() {
|
callCloneAPI() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
// 确保性别不为空,使用多重保护
|
||||||
|
let gender = this.form.gender;
|
||||||
|
if (!gender || gender === 'undefined' || gender === undefined) {
|
||||||
|
console.warn('性别为空,尝试重新获取');
|
||||||
|
// 如果还是空,使用默认值
|
||||||
|
gender = 'female';
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestData = {
|
||||||
|
audio_url: this.audioUrl,
|
||||||
|
voice_name: this.cloneVoiceName,
|
||||||
|
gender: gender
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('调用克隆 API,请求数据:', JSON.stringify(requestData));
|
||||||
|
console.log('性别值类型:', typeof gender, '值:', gender);
|
||||||
|
|
||||||
uni.request({
|
uni.request({
|
||||||
url: this.baseURLPy + '/config/voices/clone',
|
url: this.baseURLPy + '/config/voices/clone',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -385,19 +594,19 @@ export default {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'token': uni.getStorageSync("token") || "",
|
'token': uni.getStorageSync("token") || "",
|
||||||
},
|
},
|
||||||
data: {
|
data: requestData,
|
||||||
audio_url: this.audioUrl,
|
|
||||||
voice_name: this.cloneVoiceName,
|
|
||||||
gender: this.form.gender || 'female'
|
|
||||||
},
|
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
|
console.log('克隆 API 响应:', res);
|
||||||
if (res.data.code === 1) {
|
if (res.data.code === 1) {
|
||||||
resolve(res.data.data);
|
resolve(res.data.data);
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(res.data.message));
|
reject(new Error(res.data.message || res.data.msg || '克隆失败'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fail: reject
|
fail: (error) => {
|
||||||
|
console.error('克隆 API 请求失败:', error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -473,8 +682,12 @@ export default {
|
||||||
url: this.baseURLPy + `/config/voices/clone/${this.cloneVoiceId}/save`,
|
url: this.baseURLPy + `/config/voices/clone/${this.cloneVoiceId}/save`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
header: {
|
header: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
'token': uni.getStorageSync("token") || "",
|
'token': uni.getStorageSync("token") || "",
|
||||||
},
|
},
|
||||||
|
data: {
|
||||||
|
display_name: this.cloneVoiceName // 传递用户输入的中文名称
|
||||||
|
},
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.data.code === 1) {
|
if (res.data.code === 1) {
|
||||||
resolve(res.data.data);
|
resolve(res.data.data);
|
||||||
|
|
@ -786,4 +999,47 @@ page {
|
||||||
.clone-btn.disabled {
|
.clone-btn.disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 音频输入方式切换 */
|
||||||
|
.audio-mode-switch {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
border: 2rpx solid #E0E0E0;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
background: #F9F9F9;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-btn.active {
|
||||||
|
background: linear-gradient(135deg, #9F47FF 0%, #0053FA 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* URL 输入区域 */
|
||||||
|
.url-input-area {
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-tip {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
padding-left: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-size {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 5rpx;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -316,11 +316,11 @@ export const SessionInit = (data) => request({
|
||||||
data: data
|
data: data
|
||||||
},2)//恋人聊天初始化
|
},2)//恋人聊天初始化
|
||||||
|
|
||||||
export const SessionSend = (data) => request({
|
export const SessionSend = (data, isShowLoad = false) => request({
|
||||||
url: '/chat/send',
|
url: '/chat/send',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: data
|
data: data
|
||||||
},2)//恋人聊天发送
|
},2, isShowLoad)//恋人聊天发送
|
||||||
|
|
||||||
export const SessionSendImage = (data) => request({
|
export const SessionSendImage = (data) => request({
|
||||||
url: '/chat/send-image',
|
url: '/chat/send-image',
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
35061
xunifriend_RaeeC/runtime/log/202602/1769929952-01.log
Normal file
35061
xunifriend_RaeeC/runtime/log/202602/1769929952-01.log
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -4,7 +4,16 @@
|
||||||
4. 增加恋人消息编辑功能,更新**数据库**,加上一些编辑消息、编辑时间相关字段。用户编辑消息之后恋人不回答,只会更新记忆和摘要的数据库,下一次回答的时候会重新引用这个更新后的记忆。
|
4. 增加恋人消息编辑功能,更新**数据库**,加上一些编辑消息、编辑时间相关字段。用户编辑消息之后恋人不回答,只会更新记忆和摘要的数据库,下一次回答的时候会重新引用这个更新后的记忆。
|
||||||
- [ ] 礼物、换装、音色样式更改,但是还未更新数据库
|
- [ ] 礼物、换装、音色样式更改,但是还未更新数据库
|
||||||
5. 恋人消息回复增加思考中...
|
5. 恋人消息回复增加思考中...
|
||||||
- [ ] 恋人消息回复和消息编辑都需要测试
|
- [x] 恋人消息回复和消息编辑都需要测试
|
||||||
6. 将Hbuilder的AppId更换成自己的,原本的保留(__UNI__1F3C178)。还是无法正常编译,将下面的插件注释掉不用,Agora-RTC:音视频插件和AudioRecode:录音插件。
|
6. 将Hbuilder的AppId更换成自己的,原本的保留(__UNI__1F3C178)。还是无法正常编译,将下面的插件注释掉不用,Agora-RTC:音视频插件和AudioRecode:录音插件。
|
||||||
- [ ] 克隆音色API填写然后给你测试
|
- [x] 克隆音色API填写然后给你测试
|
||||||
7. 二维码推广功能:创建邀请码邀请新用户,但是没有二维码生成API
|
7. 二维码推广功能:创建邀请码邀请新用户,但是没有二维码生成API
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
上线需调整
|
||||||
|
|
||||||
|
1. oss存储桶:用来存放播放的音色
|
||||||
|
2. DashCope阿里云模型api:sk-xxx 并启用下面模型
|
||||||
|
- qwen-plus:AI对话聊天
|
||||||
|
- cosyvoice-v2:音色克隆
|
||||||
Loading…
Reference in New Issue
Block a user