package user import ( "context" "strings" "time" "yunyan/comm" "yunyan/lego/sys/log" "yunyan/pb" ) // @Summary 实时机器翻译 // @Description 同声传译/面对面翻译用;服务端调用后台「第三方服务配置」里启用的 MT 服务 // @Tags User // @Accept json // @Produce json // @Security BearerAuth // @Param user body pb.UserTranslateReq true "翻译请求" // @Success 200 {object} comm.HttpResult{data=pb.UserTranslateResp} "成功返回" // @Router /api/home/user_translate [post] // // 语言码:客户端传 BCP-47(zh-CN/en-US),这里统一归一成服务商要的短码。 // 阿里云对语言码传错不会报错,只会返回空或原文,所以归一放服务端收口。 func (this *apiComp) Translate(session comm.IUserSession, req *pb.UserTranslateReq) (resp *pb.UserTranslateResp, errdata *pb.ErrorData) { text := strings.TrimSpace(req.Text) to := comm.BCP47ToShortLang(req.To) from := comm.BCP47ToShortLang(req.From) if text == "" || to == "" { errdata = &pb.ErrorData{Code: pb.ErrorCode_TranslateParamInvalid, Message: "原文或目标语言为空"} return } // 同语种直接回原文,别浪费一次调用(同传里源=目标的情况很常见) if from != "" && from == to { resp = &pb.UserTranslateResp{Text: req.Text} return } // VIP 是翻译的准入闸门(自购 或 当前绑定设备赋予,见 comm.ResolveVip)。 // 文本/图片翻译与三段式(同传/面对面)的机器翻译那一跳都经过这里,拦住即整体不可用; // 通话翻译走端到端直连服务商,拦不到——它的服务端手段是不下发凭据(svcresolve.go)。 if vip, _, verr := comm.ResolveUserVip(session.GetUserId(), nil); verr == nil && !vip.Active(time.Now().Unix()) { errdata = &pb.ErrorData{Code: pb.ErrorCode_VipRequired, Message: "翻译功能需要会员,请续费或绑定带会员的设备"} return } svcId, provider, tr, err := this.module.translatemodel.resolveTranslator() if err != nil { this.module.Error("Translate: 翻译服务解析失败", log.Field{Key: "svc", Value: svcId}, log.Field{Key: "provider", Value: provider}, log.Field{Key: "err", Value: err.Error()}) errdata = &pb.ErrorData{Code: pb.ErrorCode_TranslateNotConfigured, Message: "翻译服务不可用"} return } if tr == nil { // 没配,或者配的是还没接的服务商 this.module.Warn("Translate: 无可用翻译服务", log.Field{Key: "svc", Value: svcId}, log.Field{Key: "provider", Value: provider}) errdata = &pb.ErrorData{Code: pb.ErrorCode_TranslateNotConfigured, Message: "未配置机器翻译服务"} return } // from 为空即自动检测:交给服务商识别,省掉一次单独的语种识别调用, // 检测出的短码随响应回带,客户端据此把「自动检测」显示成具体语种。 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() translated, detected, err := tr.TranslateDetect(ctx, from, to, text) if err != nil { this.module.Error("Translate: 服务商调用失败", log.Field{Key: "uid", Value: session.GetUserId()}, log.Field{Key: "svc", Value: svcId}, log.Field{Key: "from", Value: from}, log.Field{Key: "to", Value: to}, log.Field{Key: "err", Value: err.Error()}) errdata = &pb.ErrorData{Code: pb.ErrorCode_TranslateProviderError, Message: "翻译失败,请稍后再试"} return } if strings.TrimSpace(translated) == "" { // 返回空通常意味着语言码不被支持——报错比悄悄返回空串强, // 否则客户端会显示一片空白,谁也不知道发生了什么。 this.module.Warn("Translate: 服务商返回空结果", log.Field{Key: "svc", Value: svcId}, log.Field{Key: "from", Value: from}, log.Field{Key: "to", Value: to}) errdata = &pb.ErrorData{Code: pb.ErrorCode_TranslateProviderError, Message: "翻译结果为空"} return } resp = &pb.UserTranslateResp{Text: translated, Provider: provider, Svcid: svcId, DetectedFrom: detected} return }