You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
257 lines
7.2 KiB
257 lines
7.2 KiB
package filetrans
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
submitURL = "https://dashscope.aliyuncs.com/api/v1/services/audio/asr/transcription"
|
|
queryURL = "https://dashscope.aliyuncs.com/api/v1/tasks/"
|
|
)
|
|
|
|
func newSys(options Options) (sys *FileTrans, err error) {
|
|
sys = &FileTrans{
|
|
options: options,
|
|
client: &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
}
|
|
return
|
|
}
|
|
|
|
type FileTrans struct {
|
|
options Options
|
|
client *http.Client
|
|
}
|
|
|
|
// ==================== 提交任务 ====================
|
|
|
|
// submitRequest DashScope 提交转写任务请求体
|
|
type submitRequest struct {
|
|
Model string `json:"model"`
|
|
Input submitRequestInput `json:"input"`
|
|
Parameters submitRequestParams `json:"parameters"`
|
|
}
|
|
|
|
type submitRequestInput struct {
|
|
FileURLs []string `json:"file_urls"`
|
|
}
|
|
|
|
type submitRequestParams struct {
|
|
LanguageHints []string `json:"language_hints,omitempty"`
|
|
SpeakerCount *int `json:"speaker_count,omitempty"`
|
|
DiarizationEnabled bool `json:"diarization_enabled"`
|
|
NotifyURL string `json:"notify_url,omitempty"`
|
|
}
|
|
|
|
// submitResponse DashScope 提交任务的响应
|
|
type submitResponse struct {
|
|
RequestID string `json:"request_id"`
|
|
Output struct {
|
|
TaskID string `json:"task_id"`
|
|
TaskStatus string `json:"task_status"`
|
|
} `json:"output"`
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// ==================== 查询任务 ====================
|
|
|
|
// queryResponse DashScope 查询任务的响应
|
|
type queryResponse struct {
|
|
RequestID string `json:"request_id"`
|
|
Output struct {
|
|
TaskID string `json:"task_id"`
|
|
TaskStatus string `json:"task_status"`
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
TaskMetrics struct {
|
|
Total int `json:"TOTAL"`
|
|
Succeeded int `json:"SUCCEEDED"`
|
|
Failed int `json:"FAILED"`
|
|
} `json:"task_metrics"`
|
|
Results []taskResult `json:"results"`
|
|
} `json:"output"`
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type taskResult struct {
|
|
FileURL string `json:"file_url"`
|
|
TranscriptionURL string `json:"transcription_url"`
|
|
SubtaskStatus string `json:"subtask_status"`
|
|
FailureReason string `json:"failure_reason,omitempty"`
|
|
}
|
|
|
|
// transcriptionResult 转写结果 JSON(从 transcription_url 获取)
|
|
type transcriptionResult struct {
|
|
FileURL string `json:"file_url"`
|
|
Properties struct {
|
|
AudioFormat string `json:"audio_format"`
|
|
Channels []int `json:"channels"`
|
|
OriginalSamplingRate int `json:"original_sampling_rate"`
|
|
OriginalDurationInMs int64 `json:"original_duration_in_milliseconds"`
|
|
} `json:"properties"`
|
|
Transcripts []transcript `json:"transcripts"`
|
|
}
|
|
|
|
type transcript struct {
|
|
ChannelID int `json:"channel_id"`
|
|
Sentences []sentence `json:"sentences"`
|
|
}
|
|
|
|
type sentence struct {
|
|
Text string `json:"text"`
|
|
BeginTime int64 `json:"begin_time"`
|
|
EndTime int64 `json:"end_time"`
|
|
SpeakerID any `json:"speaker_id,omitempty"` // 开启说话人分离时为数字,关闭时为字符串
|
|
}
|
|
|
|
// ==================== 实现 ====================
|
|
|
|
func (this *FileTrans) CreateTask(audioURL, language string, enableSpeaker bool, callbackURL string) (taskID string, err error) {
|
|
params := submitRequestParams{
|
|
DiarizationEnabled: enableSpeaker,
|
|
NotifyURL: callbackURL,
|
|
}
|
|
if language != "" {
|
|
params.LanguageHints = []string{language}
|
|
}
|
|
|
|
reqBody := submitRequest{
|
|
Model: "paraformer-v2",
|
|
Input: submitRequestInput{
|
|
FileURLs: []string{audioURL},
|
|
},
|
|
Parameters: params,
|
|
}
|
|
|
|
bodyBytes, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return "", fmt.Errorf("marshal request: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", submitURL, strings.NewReader(string(bodyBytes)))
|
|
if err != nil {
|
|
return "", fmt.Errorf("new request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+this.options.ApiKey)
|
|
req.Header.Set("X-DashScope-Async", "enable")
|
|
|
|
resp, err := this.client.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("do request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", fmt.Errorf("read response: %w", err)
|
|
}
|
|
|
|
var result submitResponse
|
|
if err = json.Unmarshal(data, &result); err != nil {
|
|
return "", fmt.Errorf("unmarshal response: %w", err)
|
|
}
|
|
|
|
if result.Code != "" {
|
|
return "", fmt.Errorf("create task failed: code=%s, message=%s", result.Code, result.Message)
|
|
}
|
|
|
|
return result.Output.TaskID, nil
|
|
}
|
|
|
|
func (this *FileTrans) QueryTask(taskID string) (status string, contexts []ContextStruct, err error) {
|
|
reqURL := queryURL + taskID
|
|
req, err := http.NewRequest("GET", reqURL, nil)
|
|
if err != nil {
|
|
return StatusFailed, nil, fmt.Errorf("new request: %w", err)
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+this.options.ApiKey)
|
|
|
|
resp, err := this.client.Do(req)
|
|
if err != nil {
|
|
return StatusFailed, nil, fmt.Errorf("do request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return StatusFailed, nil, fmt.Errorf("read response: %w", err)
|
|
}
|
|
|
|
var result queryResponse
|
|
if err = json.Unmarshal(data, &result); err != nil {
|
|
return StatusFailed, nil, fmt.Errorf("unmarshal response: %w", err)
|
|
}
|
|
|
|
if result.Code != "" {
|
|
return StatusFailed, nil, fmt.Errorf("query failed: code=%s, message=%s", result.Code, result.Message)
|
|
}
|
|
|
|
switch result.Output.TaskStatus {
|
|
case "SUCCEEDED":
|
|
contexts, err = this.fetchTranscriptionResults(result.Output.Results)
|
|
if err != nil {
|
|
return StatusFailed, nil, err
|
|
}
|
|
return StatusSuccess, contexts, nil
|
|
case "FAILED":
|
|
reason := result.Output.Message
|
|
if reason == "" && len(result.Output.Results) > 0 {
|
|
reason = result.Output.Results[0].FailureReason
|
|
}
|
|
if reason == "" {
|
|
reason = result.Message
|
|
}
|
|
return StatusFailed, nil, fmt.Errorf("task failed: %s", reason)
|
|
case "PENDING", "RUNNING":
|
|
return StatusRunning, nil, nil
|
|
default:
|
|
// UNKNOWN / 任务已过期(DashScope 任务结果仅保留 24 小时)/ 其他未知状态
|
|
return StatusFailed, nil, fmt.Errorf("task unavailable: task_status=%s output.code=%s output.message=%s top.code=%s top.message=%s raw=%s",
|
|
result.Output.TaskStatus, result.Output.Code, result.Output.Message, result.Code, result.Message, string(data))
|
|
}
|
|
}
|
|
|
|
// fetchTranscriptionResults 从 transcription_url 下载转写结果并解析
|
|
func (this *FileTrans) fetchTranscriptionResults(results []taskResult) (contexts []ContextStruct, err error) {
|
|
contexts = make([]ContextStruct, 0)
|
|
for _, r := range results {
|
|
if r.SubtaskStatus != "SUCCEEDED" || r.TranscriptionURL == "" {
|
|
continue
|
|
}
|
|
resp, err := this.client.Get(r.TranscriptionURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetch transcription: %w", err)
|
|
}
|
|
data, err := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read transcription: %w", err)
|
|
}
|
|
|
|
var tr transcriptionResult
|
|
if err = json.Unmarshal(data, &tr); err != nil {
|
|
return nil, fmt.Errorf("unmarshal transcription: %w", err)
|
|
}
|
|
|
|
for _, t := range tr.Transcripts {
|
|
for _, s := range t.Sentences {
|
|
contexts = append(contexts, ContextStruct{
|
|
Content: s.Text,
|
|
StartTime: s.BeginTime,
|
|
EndTime: s.EndTime,
|
|
Speaker: fmt.Sprintf("%v", s.SpeakerID),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|