| package handler |
|
|
| import ( |
| "fmt" |
| "io" |
| "strings" |
|
|
| "aurora/httpclient/bogdanfinn" |
| "aurora/internal/accounts" |
| "aurora/internal/chatgpt" |
| "aurora/internal/config" |
| officialtypes "aurora/typings/official" |
| chatgptrequestconverter "aurora/conversion/requests/chatgpt" |
|
|
| "github.com/gin-gonic/gin" |
| ) |
|
|
| type AudioHandler struct { |
| accountPool *accounts.Pool |
| cfg *config.Config |
| } |
|
|
| func NewAudioHandler(pool *accounts.Pool, cfg *config.Config) *AudioHandler { |
| return &AudioHandler{accountPool: pool, cfg: cfg} |
| } |
|
|
| |
|
|
| var ttsFmtMap = map[string]string{ |
| "mp3": "mp3", |
| "opus": "opus", |
| "aac": "aac", |
| "flac": "aac", |
| "wav": "aac", |
| "pcm": "aac", |
| } |
|
|
| var ttsTypeMap = map[string]string{ |
| "mp3": "audio/mpeg", |
| "opus": "audio/ogg", |
| "aac": "audio/aac", |
| } |
|
|
| var ttsVoiceMap = map[string]string{ |
| "alloy": "cove", |
| "ash": "fathom", |
| "coral": "vale", |
| "echo": "ember", |
| "fable": "breeze", |
| "onyx": "orbit", |
| "nova": "maple", |
| "sage": "glimmer", |
| "shimmer": "juniper", |
| } |
|
|
| func (h *AudioHandler) TTS(c *gin.Context) { |
| var original_request officialtypes.TTSAPIRequest |
| err := c.BindJSON(&original_request) |
| if err != nil { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": "Request must be proper JSON", |
| "type": "invalid_request_error", |
| "param": nil, |
| "code": err.Error(), |
| }}) |
| return |
| } |
| if original_request.Input == "" { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": "Missing required parameter: input", |
| "type": "invalid_request_error", |
| "param": "input", |
| "code": "missing_required_parameter", |
| }}) |
| return |
| } |
|
|
| account, _, err := resolveAccount(c, h.accountPool, h.cfg, true) |
| if err != nil { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": err.Error(), |
| "type": "authorization_error", |
| "param": "Authorization", |
| "code": 400, |
| }}) |
| return |
| } |
| if account == nil || account.Token == "" { |
| c.JSON(400, gin.H{"error": "TTS requires a logged-in ChatGPT access token."}) |
| return |
| } |
| if !account.Type.Satisfies(accounts.CapTTS) { |
| c.JSON(403, gin.H{"error": "TTS requires a logged-in ChatGPT account."}) |
| return |
| } |
|
|
| proxyUrl := account.Proxy |
| client := setupClientWithProxy(proxyUrl) |
|
|
| |
| translated_request := chatgptrequestconverter.ConvertTTSAPIRequest(original_request.Input) |
| clientState := chatgpt.NewChatClientState() |
| clientState.ConversationID = translated_request.ConversationID |
| clientState.ParentMessageID = translated_request.ParentMessageID |
|
|
| response, wsConn, _, status, err := conversationClientOrder(&client, account, translated_request, proxyUrl, false, clientState, h.accountPool) |
| if err != nil { |
| c.JSON(status, gin.H{"error": gin.H{ |
| "message": err.Error(), |
| "type": "request_conversion_error", |
| "param": "model", |
| "code": "request_conversion_error", |
| }}) |
| return |
| } |
| defer response.Body.Close() |
| if chatgpt.Handle_request_error(c, response) { |
| if wsConn != nil { |
| wsConn.Close() |
| } |
| return |
| } |
|
|
| msgId, convId := chatgpt.HandlerTTS(response, original_request.Input) |
| if msgId == "" || convId == "" { |
| c.JSON(500, gin.H{"error": "failed to get TTS message id"}) |
| return |
| } |
| defer chatgpt.RemoveConversation(client, account, convId, proxyUrl) |
|
|
| format := ttsFmtMap[original_request.Format] |
| if format == "" { |
| format = "aac" |
| } |
| voice := ttsVoiceMap[original_request.Voice] |
| if voice == "" { |
| voice = "cove" |
| } |
| data, status, err := chatgpt.GetTTS(client, account, msgId, convId, voice, format, proxyUrl) |
| if err != nil { |
| c.JSON(status, gin.H{"error": gin.H{ |
| "message": err.Error(), |
| "type": "synthesize_request_error", |
| "param": nil, |
| "code": status, |
| }}) |
| return |
| } |
| c.Data(200, ttsTypeMap[format], data) |
| } |
|
|
| |
|
|
| var transcriptionsSupportedFormats = map[string]string{ |
| "json": "application/json", |
| "text": "text/plain", |
| "verbose_json": "application/json", |
| } |
|
|
| func (h *AudioHandler) Transcriptions(c *gin.Context) { |
| h.handleTranscription(c, false) |
| } |
|
|
| func (h *AudioHandler) Translations(c *gin.Context) { |
| h.handleTranscription(c, true) |
| } |
|
|
| func (h *AudioHandler) handleTranscription(c *gin.Context, isTranslation bool) { |
| contentType := strings.Split(c.GetHeader("Content-Type"), ";")[0] |
| contentType = strings.ToLower(strings.TrimSpace(contentType)) |
| if !strings.HasPrefix(contentType, "multipart/form-data") { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": "Request must be multipart/form-data", |
| "type": "invalid_request_error", |
| "param": "Content-Type", |
| "code": "invalid_content_type", |
| }}) |
| return |
| } |
|
|
| if err := c.Request.ParseMultipartForm(50 << 20); err != nil { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": "Failed to parse multipart form: " + err.Error(), |
| "type": "invalid_request_error", |
| "param": nil, |
| "code": "parse_error", |
| }}) |
| return |
| } |
|
|
| model := strings.TrimSpace(c.Request.FormValue("model")) |
| language := strings.TrimSpace(c.Request.FormValue("language")) |
| prompt := c.Request.FormValue("prompt") |
| responseFormat := strings.TrimSpace(c.Request.FormValue("response_format")) |
|
|
| if model == "" { |
| model = "whisper-1" |
| } |
| if responseFormat == "" { |
| responseFormat = "json" |
| } |
|
|
| respContentType, formatOK := transcriptionsSupportedFormats[responseFormat] |
| if !formatOK { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": fmt.Sprintf("Unsupported response_format: %s. Supported values: json, text, verbose_json", responseFormat), |
| "type": "invalid_request_error", |
| "param": "response_format", |
| "code": "invalid_response_format", |
| }}) |
| return |
| } |
|
|
| if len(prompt) > 1000 { |
| prompt = prompt[:1000] |
| } |
|
|
| formFile, fileHeader, err := c.Request.FormFile("file") |
| if err != nil { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": "Missing required multipart field: file", |
| "type": "invalid_request_error", |
| "param": "file", |
| "code": "missing_required_parameter", |
| }}) |
| return |
| } |
| defer formFile.Close() |
|
|
| audioData, err := io.ReadAll(formFile) |
| if err != nil { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": err.Error(), |
| "type": "invalid_request_error", |
| "param": "file", |
| "code": "file_read_error", |
| }}) |
| return |
| } |
| if len(audioData) == 0 { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": "Uploaded audio file is empty", |
| "type": "invalid_request_error", |
| "param": "file", |
| "code": "empty_file", |
| }}) |
| return |
| } |
|
|
| account, _, err := resolveAccount(c, h.accountPool, h.cfg, true) |
| if err != nil { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": err.Error(), |
| "type": "authorization_error", |
| "param": "Authorization", |
| "code": 400, |
| }}) |
| return |
| } |
| if account == nil || account.Token == "" { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": "Audio transcription requires a logged-in ChatGPT access token.", |
| "type": "invalid_request_error", |
| "param": "file", |
| "code": "missing_access_token", |
| }}) |
| return |
| } |
| if !account.Type.Satisfies(accounts.CapTranscribe) { |
| c.JSON(403, gin.H{"error": "Audio transcription requires a logged-in ChatGPT account."}) |
| return |
| } |
|
|
| proxyUrl := account.Proxy |
| mimeType := fileHeader.Header.Get("Content-Type") |
| if mimeType == "" { |
| mimeType = "audio/mpeg" |
| } |
|
|
| |
| client, ok := account.Client.(*bogdanfinn.TlsClient) |
| if !ok || client == nil { |
| client = bogdanfinn.NewStdClient() |
| client.SetCookies("https://chatgpt.com", chatgpt.BasicCookies) |
| if proxyUrl != "" { |
| client.SetProxy(proxyUrl) |
| } |
| } |
|
|
| text, status, err := chatgpt.TranscribeAudio(client, account, proxyUrl, audioData, fileHeader.Filename, mimeType, language) |
| if err != nil { |
| c.JSON(status, gin.H{"error": gin.H{ |
| "message": err.Error(), |
| "type": "transcription_error", |
| "param": nil, |
| "code": "transcription_error", |
| }}) |
| return |
| } |
|
|
| switch responseFormat { |
| case "json": |
| c.JSON(200, officialtypes.TranscriptionResponse{Text: text}) |
| case "text": |
| c.Data(200, "text/plain; charset=utf-8", []byte(text)) |
| case "verbose_json": |
| c.JSON(200, officialtypes.VerboseTranscriptionResponse{ |
| Task: "transcribe", |
| Language: language, |
| Duration: 0, |
| Text: text, |
| Segments: []officialtypes.TranscriptionSegment{}, |
| Words: []officialtypes.TranscriptionWord{}, |
| }) |
| default: |
| c.Data(200, respContentType, []byte(text)) |
| } |
| } |
|
|