refactor: 收口技能与专员目录定义
统一 skill 与 specialist 清单,移除旧的 office 执行包与下线对象,并补入 text_to_speech、customer_followup 等当前目录定义与种子数据。
This commit is contained in:
@@ -199,7 +199,7 @@ func ExecuteOfficeSkillOCR(c *gin.Context) {
|
||||
TaskID: task.ID,
|
||||
SpecialistKey: task.SpecialistKey,
|
||||
ActionKey: "ocr-understanding",
|
||||
ActionTitle: "OCR / 图文理解",
|
||||
ActionTitle: "图片/扫描件智能识别",
|
||||
ActionType: "ocr",
|
||||
Status: "done",
|
||||
InputJSON: string(inputJSON),
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
package skillapi
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"eai_agentplatform/backend/internal/config"
|
||||
"eai_agentplatform/backend/internal/middleware"
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
specialistruntime "eai_agentplatform/backend/internal/specialists/runtime"
|
||||
"eai_agentplatform/backend/internal/store"
|
||||
"eai_agentplatform/backend/internal/web"
|
||||
)
|
||||
|
||||
// ttsDefaultVoice 默认中文女声(edge-tts 自带音色)。
|
||||
const ttsDefaultVoice = "zh-CN-XiaoxiaoNeural"
|
||||
|
||||
// ttsAudioDir 存放合成 MP3 的目录,与公众号配图一致放在数据库同级的 tts_audio 下。
|
||||
func ttsAudioDir() string {
|
||||
cfg := config.Load()
|
||||
return filepath.Join(filepath.Dir(cfg.DBPath), "tts_audio")
|
||||
}
|
||||
|
||||
func defaultTTSText(text string) string {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return "这是一个文字转语音的示例。"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// ExecuteOfficeSkillTTS POST /api/skills/office/tts —— 文字转语音。
|
||||
// body: { "task_id": <任务ID>, "text": "要朗读的文字", "voice": "音色(可空)", "rate": "语速(可空, 如 +0% / -10%)" }
|
||||
func ExecuteOfficeSkillTTS(c *gin.Context) {
|
||||
user := middleware.CurrentUser(c)
|
||||
if user == nil {
|
||||
web.Fail(c, web.NewAuthError("未登录"))
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
TaskID uint `json:"task_id"`
|
||||
Text string `json:"text"`
|
||||
Voice string `json:"voice"`
|
||||
Rate string `json:"rate"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
web.Fail(c, web.NewBadRequest("请求参数错误"))
|
||||
return
|
||||
}
|
||||
if req.TaskID == 0 {
|
||||
web.Fail(c, web.NewBadRequest("task_id 必填"))
|
||||
return
|
||||
}
|
||||
|
||||
text := defaultTTSText(req.Text)
|
||||
voice := strings.TrimSpace(req.Voice)
|
||||
if voice == "" {
|
||||
voice = ttsDefaultVoice
|
||||
}
|
||||
rate := strings.TrimSpace(req.Rate)
|
||||
|
||||
task, ok := loadMyOwnedTask(c, user, req.TaskID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
dir := ttsAudioDir()
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
web.Fail(c, web.NewBadRequest("无法创建音频目录"))
|
||||
return
|
||||
}
|
||||
sum := sha1.Sum([]byte(text + "|" + voice))
|
||||
filename := fmt.Sprintf("task_%d_%s_%x.mp3", task.ID, now.Format("20060102-150405"), sum[:6])
|
||||
audioPath := filepath.Join(dir, filename)
|
||||
|
||||
args := []string{"-m", "edge_tts", "--text", text, "--voice", voice, "--write-media", audioPath}
|
||||
if rate != "" {
|
||||
args = append(args, "--rate", rate)
|
||||
}
|
||||
cmd := exec.Command("python3", args...)
|
||||
var stderr strings.Builder
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
web.Fail(c, web.NewBadRequest("语音合成失败:"+firstNonEmpty(strings.TrimSpace(stderr.String()), err.Error())))
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(audioPath); err != nil {
|
||||
web.Fail(c, web.NewBadRequest("语音合成失败:未生成音频文件"))
|
||||
return
|
||||
}
|
||||
audioURL := "/api/tts/audio/" + filename
|
||||
|
||||
inputJSON, _ := json.Marshal(gin.H{"text": text, "voice": voice, "rate": rate})
|
||||
outputJSON, _ := json.Marshal(gin.H{
|
||||
"summary": "已将文本合成为语音,可在线播放或下载。",
|
||||
"text": text,
|
||||
"voice": voice,
|
||||
"audio_url": audioURL,
|
||||
})
|
||||
logsJSON, _ := json.Marshal([]string{
|
||||
"接收文字转语音请求",
|
||||
fmt.Sprintf("调用本地 TTS 引擎(voice=%s)", voice),
|
||||
"已生成 MP3 音频",
|
||||
})
|
||||
artifactPayloadJSON, _ := json.Marshal(gin.H{
|
||||
"text": text,
|
||||
"voice": voice,
|
||||
"audio_url": audioURL,
|
||||
"format": "mp3",
|
||||
})
|
||||
sourceRefsJSON, _ := json.Marshal([]string{"本地 edge-tts"})
|
||||
|
||||
var run model.TaskRun
|
||||
var artifact model.TaskArtifact
|
||||
err := store.DB.Transaction(func(tx *gorm.DB) error {
|
||||
run = model.TaskRun{
|
||||
TaskID: task.ID,
|
||||
SpecialistKey: task.SpecialistKey,
|
||||
ActionKey: "text-to-speech",
|
||||
ActionTitle: "文字转语音",
|
||||
ActionType: "audio",
|
||||
Status: "done",
|
||||
InputJSON: string(inputJSON),
|
||||
OutputJSON: string(outputJSON),
|
||||
LogsJSON: string(logsJSON),
|
||||
StartedAt: now,
|
||||
FinishedAt: &now,
|
||||
}
|
||||
if err := tx.Create(&run).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
artifact = model.TaskArtifact{
|
||||
TaskID: task.ID,
|
||||
SpecialistKey: task.SpecialistKey,
|
||||
Title: fmt.Sprintf("%s - 语音合成结果", task.Title),
|
||||
ArtifactType: "audio",
|
||||
Status: specialistruntime.ArtifactStatusDraft,
|
||||
ContentText: text,
|
||||
ContentJSON: string(artifactPayloadJSON),
|
||||
SourceRefsJSON: string(sourceRefsJSON),
|
||||
CreatedByRunID: &run.ID,
|
||||
}
|
||||
if err := tx.Create(&artifact).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
task.CurrentRunID = &run.ID
|
||||
task.CurrentResult = "已将文本合成为语音并生成 MP3。"
|
||||
task.Status = specialistruntime.TaskStatusDraft
|
||||
task.LastTriggeredAt = &now
|
||||
return tx.Save(&task).Error
|
||||
})
|
||||
if err != nil {
|
||||
web.Fail(c, web.NewBadRequest("保存结果失败:"+err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
freshTask, _ := reloadTask(task.ID)
|
||||
web.OK(c, gin.H{
|
||||
"audio_url": audioURL,
|
||||
"text": text,
|
||||
"voice": voice,
|
||||
"rate": rate,
|
||||
"run": run,
|
||||
"artifact": artifact,
|
||||
"task": freshTask,
|
||||
})
|
||||
}
|
||||
|
||||
// ServeTTSAudio GET /api/tts/audio/:filename —— 提供生成的 MP3 音频文件。
|
||||
func ServeTTSAudio(c *gin.Context) {
|
||||
filename := filepath.Base(strings.TrimSpace(c.Param("filename")))
|
||||
if filename == "." || filename == "" {
|
||||
web.Fail(c, web.NewNotFoundError("音频不存在"))
|
||||
return
|
||||
}
|
||||
fullPath := filepath.Join(ttsAudioDir(), filename)
|
||||
if _, err := os.Stat(fullPath); err != nil {
|
||||
web.Fail(c, web.NewNotFoundError("音频不存在"))
|
||||
return
|
||||
}
|
||||
c.File(fullPath)
|
||||
}
|
||||
Reference in New Issue
Block a user