├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── workflows │ └── deploy-docker.yml ├── .idea ├── .gitignore ├── luma-api.iml ├── modules.xml └── vcs.xml ├── Dockerfile ├── README.md ├── README_ZH.md ├── api.go ├── chat.go ├── common ├── envs.go ├── log.go ├── openai_tools.go ├── template.go └── utils.go ├── docker-compose.yml ├── docs ├── docs.go ├── images │ ├── cookie.jpg │ ├── token.png │ └── zanshangcode.jpg ├── swagger.json └── swagger.yaml ├── go.mod ├── go.sum ├── keep-alive.go ├── license ├── logs └── luma2api-20240618.log ├── luma-api ├── luma.go ├── main.go ├── middleware ├── auth.go ├── logger.go └── request-id.go ├── models.go ├── request.go ├── router.go └── template └── luma.yaml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 报告问题 3 | about: 使用简练详细的语言描述你遇到的问题 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **例行检查** 11 | 12 | [//]: # (方框内删除已有的空格,填 x 号) 13 | + [ ] 我已确认目前没有类似 issue 14 | + [ ] 我已确认我已升级到最新版本 15 | + [ ] 我已完整查看过项目 README,尤其是常见问题部分 16 | + [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 17 | + [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭** 18 | 19 | **问题描述** 20 | 21 | **复现步骤** 22 | 23 | **预期结果** 24 | 25 | **相关截图** 26 | 如果没有的话,请删除此节。 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: "" 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能请求 3 | about: 使用简练详细的语言描述希望加入的新功能 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **例行检查** 11 | 12 | [//]: # (方框内删除已有的空格,填 x 号) 13 | + [ ] 我已确认目前没有类似 issue 14 | + [ ] 我已确认我已升级到最新版本 15 | + [ ] 我已完整查看过项目 README,已确定现有版本无法满足需求 16 | + [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 17 | + [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭** 18 | 19 | **功能描述** 20 | 21 | **应用场景** 22 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI/CD 2 | on: 3 | push: 4 | branches: [ main ] 5 | jobs: 6 | # 构建并上传 Docker镜像 7 | build: 8 | runs-on: ubuntu-latest # 依赖的环境 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Build Image 12 | run: | 13 | docker build --platform linux/amd64 -t lumaapi/luma-api . 14 | - name: Login to Registry 15 | run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }} 16 | - name: Push Image 17 | run: | 18 | docker push lumaapi/luma-api -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 基于编辑器的 HTTP 客户端请求 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/luma-api.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang AS builder 2 | 3 | ENV GO111MODULE=on \ 4 | GOOS=linux \ 5 | GOPROXY=https://goproxy.cn,direct \ 6 | CGO_ENABLED=0 7 | 8 | WORKDIR /build 9 | ADD go.mod go.sum ./ 10 | RUN go mod download 11 | COPY . . 12 | RUN go build -ldflags "-s -w -extldflags '-static'" -o lumaApi 13 | 14 | FROM alpine:latest 15 | 16 | RUN apk update \ 17 | && apk upgrade \ 18 | && apk add --no-cache ca-certificates tzdata gcc 19 | 20 | COPY --from=builder /build/lumaApi / 21 | COPY --from=builder /build/template /template/ 22 | 23 | EXPOSE 8000 24 | 25 | ENTRYPOINT ["/lumaApi"] 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Luma API 2 | [中文](./README_ZH.md) 3 | 4 | Using Golang, convert the video capabilities of the LumaAI Dream Machine into an API format for calls, making it easier to integrate quickly into third-party systems. 5 | 6 | ## Disclaimer 7 | - This project is released on GitHub under the MIT license, free and open-source for educational purposes. 8 | 9 | # Good news 10 | I provide the Luma AI API, no deployment is required, no subscription to Luma is required. Lower price, more convenient to use the Luma API. 11 | #### Website: https://api.bltcy.ai 12 | 13 | ## Supported Features 14 | - [x] Luma API supports video generation and allows uploading reference images. 15 | - [x] Users can upload reference images in advance or provide third-party image links or base64 encoded images when submitting video tasks. 16 | - [x] API interface security verification. 17 | - [x] Simplified deployment process, supporting both docker-compose and docker. 18 | - [x] Provides standardized services compatible with OpenAI's interface format, supporting both streaming and non-streaming output. 19 | - [x] Supports customization of the OpenAI Chat response format based on Go Template syntax. 20 | - [x] Compatible with frontend projects like chat-next-web. 21 | - [x] Automatic keep-alive (max keep-alive 7 days). 22 | - [] Adapts to intermediary projects like New API. 23 | - [] Pre-detection of sensitive words. 24 | 25 | ## Supported Endpoints 26 | Video generation, supports uploading two images (start and end). 27 | [x] /generations 28 | Video extension, extends by 5 seconds each time. 29 | [x] /generations/:task_id/extend 30 | Query single video task. 31 | [x] /generations/:task_id 32 | Pre-upload images. 33 | [x] /generations/file_upload 34 | Get download URL without watermark. 35 | [x] /generations/:task_id/download_video_url 36 | Get account subscription information (balance, etc.). 37 | [x] /subscription/usage 38 | Get account information. 39 | [x] /users/me 40 | 41 | ## Web UI 42 | https://github.com/Dooy/chatgpt-web-midjourney-proxy 43 | Online 44 | https://api-chat.gptbest.vip 45 | 46 | ## API Docs 47 | 48 | http://localhost:8000/swagger/index.html 49 | 50 | #### Request example 51 | ```bash 52 | curl --location 'http://localhost:8000/luma/generations/' \ 53 | --header 'Authorization: Bearer {SECRET_TOKEN}' \ 54 | --header 'Content-Type: application/json' \ 55 | --data '{ 56 | "user_prompt": "Increase dynamic", 57 | "aspect_ratio": "16:9", 58 | "expand_prompt": true, 59 | "image_url": "https://api.bltcy.ai/logo.png" 60 | }' 61 | ``` 62 | 63 | #### Response example 64 | ```bash 65 | { 66 | "created_at": "2024-06-18T14:47:43.318498Z", 67 | "estimate_wait_seconds": null, 68 | "id": "a4a8f1ff-e59b-4969-abcb-98b3e16f003e", 69 | "liked": null, 70 | "prompt": "Increase dynamic", 71 | "state": "pending", 72 | "video": null 73 | } 74 | ``` 75 | 76 | ## Deployment 77 | 78 | ### Configuration 79 | Log in using Google Chrome https://lumalabs.ai/dream-machine/ 80 | Get cookie 81 | ![cookie](./docs/images/cookie.jpg) 82 | 83 | 84 | ### Env Environment Variables 85 | | env | description | default value | 86 | |-------------|--------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------| 87 | | COOKIE | Cookie obtained from the image above for the Luma account | None | 88 | | BASE_URL | Request API URL for Luma | https://internal-api.virginia.labs.lumalabs.ai | 89 | | PROXY | HTTP proxy | None | 90 | | PORT | Port | 8000 | 91 | | SECRET_TOKEN | Luma API seurity http header Bearer token | None | 92 | | ROTATE_LOGS | Whether to rotate logs daily | Yes | 93 | | LOG_DIR | Log output path | ./logs | 94 | | DEBUG | Whether to enable Debug logging | No | 95 | | PPROF | Whether to enable Pprof performance analysis, uses port 8005 if enabled | No | 96 | | CHAT_TIME_OUT | Chat request time out | 600 s | 97 | | CHAT_TEMPLATE_DIR | Chat template path | ./template | 98 | 99 | 100 | ### Docker Deployment 101 | Step-by-step guide on how to run a Docker container with specific environment variables and port mappings. Sensitive details like SQL names, passwords, and IP addresses are replaced with placeholders for this guide. 102 | 103 | ```bash 104 | docker run --name luma-api -d -p 8000:8000 \ 105 | -e COOKIE=xxxx \ 106 | lumaapi/luma-api 107 | ``` 108 | 109 | docker-compose deployment 110 | ```bash 111 | docker-compose pull && docker-compose up -d 112 | ``` 113 | 114 | docker-compose.yml 115 | ```bash 116 | version: '3.2' 117 | 118 | services: 119 | sunoapi: 120 | image: lumaapi/luma-api:latest 121 | container_name: luma-api 122 | restart: always 123 | ports: 124 | - "8000:8000" 125 | volumes: 126 | - ./logs:/logs 127 | environment: 128 | - COOKIE=xxxx 129 | ``` 130 | 131 | ### Local Deployment 132 | - Golang 1.20+ 133 | 134 | ```bash 135 | git clone https://github.com/LumaAI-API/Luma-API.git 136 | cd Luma-API 137 | go mod tidy 138 | ``` 139 | 140 | ## License 141 | MIT © [Luma API](./license) 142 | 143 | ## Buy Me a Coke 144 | ![zanshangcode.jpg](./docs/images/zanshangcode.jpg) 145 | 146 | This project is open-source on GitHub under the MIT license and is free of charge. If you find this project helpful, please give it a star and share it. Thank you for your support! 147 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | # Luma API 2 | 使用 Golang 将 LumaAI Dream Machine 的视频功能,转换为 API 形式进行调用,便于更快速地集成到第三方系统中。 3 | 4 | ## 免责声明 5 | - 该项目仅在 GitHub 上以 MIT 许可证发布,免费且开源,仅供学习之用。 6 | 7 | # 好消息 8 | 提供 Luma API,不需要任何部署,不需要购买 Luma 账户, 更低的价格 & 更好的体验。 9 | 网址: https://api.bltcy.ai 10 | 11 | 12 | ## 支持功能 13 | - [x] Luma API 支持视频生成、支持上传携带参考图 14 | - [x] 上传参考图片,可提前上传,也可在提交视频任务时上传第三方图片链接、base64 15 | - [x] API 接口安全校验 16 | - [x] 简化部署流程,支持 docker-compose、docker 17 | - [x] 提供符合 OpenAI 格式的接口标准化服务,支持流式、非流输出内容 18 | - [x] 支持自定义 OpenAI Chat 返回内容格式,基于 Go Template 语法 19 | - [x] 适配 chat-next-web 等前端项目 20 | - [x] 自动保活(最多7天) 21 | - [] 适配 New API 等中转项目 22 | - [] 敏感词预检测 23 | 24 | ## 支持接口 25 | - 视频生成,支持上传首尾两张图片 26 | - [x] /generations 27 | - 视频延长,每次延长5秒 28 | - [x] /generations/:task_id/extend 29 | - 查询单视频任务 30 | - [x] /generations/:task_id 31 | - 预上传图片 32 | - [x] /generations/file_upload 33 | - 获取无水印下载地址 34 | - [x] /generations/:task_id/download_video_url 35 | - 获取账号订阅信息(余额等) 36 | - [x] /subscription/usage 37 | - 获取账号信息 38 | - [x] /users/me 39 | 40 | 41 | 42 | ## 对应界面 43 | https://github.com/Dooy/chatgpt-web-midjourney-proxy 44 | 在线体验: 45 | https://api-chat.gptbest.vip 46 | 47 | ## API 文档 48 | 49 | http://localhost:8000/swagger/index.html 50 | 51 | #### 请求示例 52 | ```bash 53 | curl --location 'http://localhost:8000/luma/generations/' \ 54 | --header 'Authorization: Bearer {SECRET_TOKEN}' \ 55 | --header 'Content-Type: application/json' \ 56 | --data '{ 57 | "user_prompt": "Increase dynamic", 58 | "aspect_ratio": "16:9", 59 | "expand_prompt": true, 60 | "image_url": "https://api.bltcy.ai/logo.png" 61 | }' 62 | ``` 63 | 64 | #### 响应示例 65 | ```bash 66 | { 67 | "created_at": "2024-06-18T14:47:43.318498Z", 68 | "estimate_wait_seconds": null, 69 | "id": "a4a8f1ff-e59b-4969-abcb-98b3e16f003e", 70 | "liked": null, 71 | "prompt": "Increase dynamic", 72 | "state": "pending", 73 | "video": null 74 | } 75 | ``` 76 | 77 | ## Deployment 78 | 79 | ### Configuration 80 | 在此登录 Google 账户 https://lumalabs.ai/dream-machine/ 81 | 选择以下其中一种方式获取凭证 82 | 1. 从浏览器中获取 cookie 83 | ![cookie](./docs/images/cookie.jpg) 84 | 85 | ### Env Environment Variables 86 | | 环境变量 | 说明 | 默认值 | 87 | | --- |----------------------------------------------------------|----------------------------| 88 | | COOKIE | 上图获取的 luma 账号的 cookie | 空 | 89 | | BASE_URL | Luma 官方请求 API URL | https://internal-api.virginia.labs.lumalabs.ai | 90 | | PROXY | Http 代理 | 空 | 91 | | PORT | 开放端口 | 8000 | 92 | | SECRET_TOKEN | Luma API 接口安全 Header Bearer token,强烈希望配置 | 空 | 93 | | ROTATE_LOGS | 日志是否按天轮转 | 是 | 94 | | LOG_DIR | 日志输出路径 | ./logs | 95 | | DEBUG | 是否开启 Debug 日志 | 否 | 96 | | PPROF | 是否开启 Pprof 性能分析,开启后 8005 端口使用 | 否 | 97 | | CHAT_TIME_OUT | Chat 请求超时时间 | 600 秒 | 98 | | CHAT_TEMPLATE_DIR | Chat 模板读取路径 | ./template | 99 | 100 | ### Docker Deployment 101 | 本教程提供如何使用特定的环境变量及端口映射来运行一个Docker容器的分步指导。为了本指南的目的,敏感信息如SQL名称、密码和IP地址将被替换为占位符。 102 | 103 | ```bash 104 | docker run --name luma-api -d -p 8000:8000 \ 105 | -e COOKIE=xxxx \ 106 | lumaapi/luma-api 107 | ``` 108 | 109 | docker-compose deployment 110 | ```bash 111 | docker-compose pull && docker-compose up -d 112 | ``` 113 | 114 | docker-compose.yml 115 | ```bash 116 | version: '3.2' 117 | 118 | services: 119 | sunoapi: 120 | image: lumaapi/luma-api:latest 121 | container_name: luma-api 122 | restart: always 123 | ports: 124 | - "8000:8000" 125 | volumes: 126 | - ./logs:/logs 127 | environment: 128 | - COOKIE=xxxx 129 | ``` 130 | 131 | ### Local Deployment 132 | - Golang 1.20+ 133 | 134 | ```bash 135 | git clone https://github.com/LumaAI-API/Luma-API.git 136 | cd Luma-API 137 | go mod tidy 138 | ``` 139 | 140 | 141 | ## License 142 | MIT © [Luma API](./license) 143 | 144 | 145 | ## 给我买瓶可乐 146 | ![zanshangcode.jpg](./docs/images/zanshangcode.jpg) 147 | 148 | 此项目开源于GitHub ,基于MIT协议且免费,没有任何形式的付费行为!如果你觉得此项目对你有帮助,请帮我点个Star并转发扩散,在此感谢你! 149 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "io" 7 | "luma-api/common" 8 | "net/http" 9 | ) 10 | 11 | // @Summary Submit luma generate video task 12 | // @Schemes 13 | // @Description 14 | // @Accept json 15 | // @Produce json 16 | // @Param body body GenRequest true "submit generate video" 17 | // @Success 200 {object} []VideoTask "generate result" 18 | // @Router /luma/generations [post] 19 | func Generations(c *gin.Context) { 20 | doGeneration(c) 21 | } 22 | 23 | // @Summary Submit luma extend generate video task 24 | // @Schemes 25 | // @Description 26 | // @Accept json 27 | // @Produce json 28 | // @Param task_id path string true "extend task id" 29 | // @Param body body GenRequest true "submit generate video" 30 | // @Success 200 {object} []VideoTask "generate result" 31 | // @Router /luma/generations/:task_id/extend [post] 32 | func ExtentGenerations(c *gin.Context) { 33 | doGeneration(c) 34 | } 35 | 36 | // @Summary Get luma generate video task 37 | // @Schemes 38 | // @Description 39 | // @Accept json 40 | // @Produce json 41 | // @Param task_id path string true "fetch single task by id" 42 | // @Success 200 {object} VideoTask "video single task" 43 | // @Router /luma/generations/{task_id} [get] 44 | func FetchByID(c *gin.Context) { 45 | id := c.Param("task_id") 46 | url := fmt.Sprintf(common.BaseUrl+GetTaskEndpoint, "/"+id) 47 | if c.Request.URL.RawQuery != "" { 48 | url = fmt.Sprintf("%s?%s", url, c.Request.URL.RawQuery) 49 | } 50 | resp, err := DoRequest("GET", url, nil, nil) 51 | if err != nil { 52 | WrapperLumaError(c, err, http.StatusInternalServerError) 53 | return 54 | } 55 | defer resp.Body.Close() 56 | 57 | c.Writer.WriteHeader(resp.StatusCode) 58 | for key, values := range resp.Header { 59 | for _, value := range values { 60 | c.Writer.Header().Add(key, value) 61 | } 62 | } 63 | // 读取响应体 64 | _, err = io.Copy(c.Writer, resp.Body) 65 | if err != nil { 66 | WrapperLumaError(c, err, http.StatusInternalServerError) 67 | return 68 | } 69 | return 70 | } 71 | 72 | // @Summary Upload image to luma 73 | // @Schemes 74 | // @Description 75 | // @Accept json 76 | // @Produce json 77 | // @Param body body UploadReq true "Upload image params" 78 | // @Success 200 {object} []FileUploadResult "upload result" 79 | // @Router /luma/generations/file_upload [post] 80 | func Upload(c *gin.Context) { 81 | var uploadParams UploadReq 82 | err := c.BindJSON(&uploadParams) 83 | if err != nil { 84 | WrapperLumaError(c, err, http.StatusBadRequest) 85 | return 86 | } 87 | 88 | res, relayErr := uploadFile(uploadParams.Url) 89 | if relayErr != nil { 90 | ReturnLumaError(c, relayErr.ErrorResp, relayErr.StatusCode) 91 | return 92 | } 93 | c.JSON(http.StatusOK, res) 94 | return 95 | } 96 | 97 | // @Summary Get video url without watermark 98 | // @Schemes 99 | // @Description 100 | // @Accept json 101 | // @Produce json 102 | // @Param task_id path string true "fetch by id" 103 | // @Success 200 {object} object "url" 104 | // @Router /luma/generations/{task_id}/download_video_url [post] 105 | func GetDownloadUrl(c *gin.Context) { 106 | taskID := c.Param("task_id") 107 | 108 | resp, err := DoRequest(http.MethodGet, fmt.Sprintf(common.BaseUrl+DownloadEndpoint, taskID), nil, nil) 109 | if err != nil { 110 | WrapperLumaError(c, err, http.StatusInternalServerError) 111 | return 112 | } 113 | defer resp.Body.Close() 114 | c.Writer.WriteHeader(resp.StatusCode) 115 | for key, values := range resp.Header { 116 | for _, value := range values { 117 | c.Writer.Header().Add(key, value) 118 | } 119 | } 120 | // 读取响应体 121 | _, err = io.Copy(c.Writer, resp.Body) 122 | if err != nil { 123 | WrapperLumaError(c, err, http.StatusInternalServerError) 124 | return 125 | } 126 | return 127 | } 128 | 129 | // @Summary Get current user info 130 | // @Schemes 131 | // @Description 132 | // @Accept json 133 | // @Produce json 134 | // @Success 200 {object} object "user info" 135 | // @Router /luma/users/me [get] 136 | func Me(c *gin.Context) { 137 | res, err := getMe() 138 | if err != nil { 139 | WrapperLumaError(c, err, http.StatusInternalServerError) 140 | return 141 | } 142 | c.JSON(http.StatusOK, res) 143 | } 144 | 145 | // @Summary Get current user subscription usage 146 | // @Schemes 147 | // @Description 148 | // @Accept json 149 | // @Produce json 150 | // @Success 200 {object} object "subscription info" 151 | // @Router /luma/subscription/usage [get] 152 | func Usage(c *gin.Context) { 153 | res, err := getUsage() 154 | if err != nil { 155 | WrapperLumaError(c, err, http.StatusInternalServerError) 156 | return 157 | } 158 | c.JSON(http.StatusOK, res) 159 | } 160 | -------------------------------------------------------------------------------- /chat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/gin-gonic/gin" 8 | "github.com/sashabaranov/go-openai" 9 | "io" 10 | "luma-api/common" 11 | "luma-api/middleware" 12 | "net/http" 13 | "time" 14 | ) 15 | 16 | const ( 17 | ModelLuma = "luma" 18 | ) 19 | 20 | func ChatCompletions(c *gin.Context) { 21 | err := checkChatConfig() 22 | if err != nil { 23 | common.ReturnOpenAIError(c, err, "config_invalid", http.StatusInternalServerError) 24 | return 25 | } 26 | 27 | chatSubmitTemp := common.Templates["chat_stream_submit"] 28 | chatTickTemp := common.Templates["chat_stream_tick"] 29 | chatRespTemp := common.Templates["chat_resp"] 30 | 31 | var requestData common.GeneralOpenAIRequest 32 | err = c.ShouldBindJSON(&requestData) 33 | if err != nil { 34 | common.ReturnOpenAIError(c, err, "parse_body_failed", http.StatusBadRequest) 35 | return 36 | } 37 | isStream := requestData.Stream 38 | model := ModelLuma 39 | 40 | chatID := "chatcmpl-" + c.GetString(middleware.RequestIdKey) 41 | 42 | prompt := requestData.Messages[len(requestData.Messages)-1].Content 43 | 44 | // do openai tools get params 45 | params := make(map[string]any) 46 | params["aspect_ratio"] = "16:9" 47 | params["expand_prompt"] = "true" 48 | params["user_prompt"] = prompt 49 | 50 | reqData, _ := json.Marshal(params) 51 | 52 | resp, err := DoRequest("POST", fmt.Sprintf(common.BaseUrl+SubmitEndpoint), bytes.NewBuffer(reqData), nil) 53 | if err != nil { 54 | common.ReturnOpenAIError(c, err, "parse_body_failed", http.StatusBadRequest) 55 | return 56 | } 57 | if resp.StatusCode >= 400 { 58 | // for error 59 | common.ReturnOpenAIError(c, err, "parse_body_failed", http.StatusBadRequest) 60 | return 61 | } 62 | data, err := io.ReadAll(resp.Body) 63 | 64 | var genTasks []VideoTask 65 | err = json.Unmarshal(data, &genTasks) 66 | 67 | if len(genTasks) == 0 { 68 | common.ReturnOpenAIError(c, err, "parse_body_failed", http.StatusBadRequest) 69 | return 70 | } 71 | taskID := genTasks[0].ID 72 | 73 | timeout := time.After(time.Duration(common.ChatTimeOut) * time.Second) 74 | tick := time.Tick(5 * time.Second) 75 | 76 | if isStream { 77 | setSSEHeaders(c) 78 | c.Writer.WriteHeader(http.StatusOK) 79 | } 80 | first := false 81 | for { 82 | select { 83 | case <-c.Request.Context().Done(): 84 | return 85 | case <-timeout: 86 | if isStream { 87 | common.SendChatData(c.Writer, model, chatID, "timeout") 88 | } else { 89 | common.ReturnOpenAIError(c, fmt.Errorf("get feed task timeout"), "request_timeout", 504) 90 | } 91 | return 92 | case <-tick: 93 | task, err := fetchTask(taskID) 94 | if err != nil { 95 | if requestData.Stream { 96 | common.SendChatData(c.Writer, model, chatID, common.GetJsonString(common.OpenAIError{ 97 | Error: common.Error{ 98 | Message: err.Error(), 99 | Code: "fetch_task_failed", 100 | }, 101 | })) 102 | } else { 103 | common.ReturnOpenAIError(c, err, "fetch_task_failed", 500) 104 | } 105 | return 106 | } 107 | if isStream { 108 | if !first { 109 | if chatSubmitTemp != nil { 110 | var byteBuf bytes.Buffer 111 | err = chatSubmitTemp.Execute(&byteBuf, task) 112 | message := byteBuf.String() 113 | common.SendChatData(c.Writer, model, chatID, message) 114 | } 115 | } else { 116 | if chatTickTemp != nil { 117 | var byteBuf bytes.Buffer 118 | err = chatTickTemp.Execute(&byteBuf, task) 119 | message := byteBuf.String() 120 | common.SendChatData(c.Writer, model, chatID, message) 121 | } 122 | } 123 | first = true 124 | } 125 | 126 | if task.State != "completed" { 127 | continue 128 | } 129 | 130 | var byteBuf bytes.Buffer 131 | err = chatRespTemp.Execute(&byteBuf, task) 132 | if err != nil { 133 | if requestData.Stream { 134 | common.SendChatData(c.Writer, model, chatID, common.GetJsonString(common.OpenAIError{ 135 | Error: common.Error{ 136 | Message: err.Error(), 137 | Code: "exec_chat_resp_template_failed", 138 | }, 139 | })) 140 | } else { 141 | common.ReturnOpenAIError(c, err, "exec_chat_resp_template_failed", 500) 142 | } 143 | return 144 | } 145 | 146 | message := byteBuf.String() 147 | if isStream { 148 | common.SendChatData(c.Writer, model, chatID, message) 149 | common.SendChatDone(c.Writer) 150 | return 151 | } else { 152 | responses := openai.ChatCompletionResponse{ 153 | ID: chatID, 154 | Object: "chat.completion", 155 | Created: time.Now().Unix(), 156 | Model: model, 157 | Choices: []openai.ChatCompletionChoice{ 158 | { 159 | Index: 0, 160 | FinishReason: "stop", 161 | Message: openai.ChatCompletionMessage{ 162 | Content: message, 163 | Role: "assistant", 164 | }, 165 | }, 166 | }, 167 | } 168 | c.JSON(http.StatusOK, responses) 169 | return 170 | } 171 | } 172 | } 173 | } 174 | 175 | func fetchTask(taskID string) (task *VideoTask, err error) { 176 | action := "/" + taskID 177 | url := fmt.Sprintf(common.BaseUrl+GetTaskEndpoint, action) 178 | resp, err := DoRequest("GET", url, nil, nil) 179 | if err != nil { 180 | return nil, err 181 | } 182 | data, err := io.ReadAll(resp.Body) 183 | if err != nil { 184 | return nil, err 185 | } 186 | err = json.Unmarshal(data, &task) 187 | if err != nil { 188 | return nil, err 189 | } 190 | return 191 | } 192 | 193 | func checkChatConfig() error { 194 | if common.ChatOpenaiApiBASE == "" { 195 | return fmt.Errorf("CHAT_OPENAI_BASE is empty") 196 | } 197 | if common.ChatOpenaiApiKey == "" { 198 | return fmt.Errorf("CHAT_OPENAI_KEY is empty") 199 | } 200 | _, ok := common.Templates["chat_resp"] 201 | if !ok { 202 | return fmt.Errorf("chat_resp template not found") 203 | } 204 | return nil 205 | } 206 | 207 | func setSSEHeaders(c *gin.Context) { 208 | c.Writer.Header().Set("Content-Type", "text/event-stream") 209 | c.Writer.Header().Set("Cache-Control", "no-cache") 210 | c.Writer.Header().Set("Connection", "keep-alive") 211 | c.Writer.Header().Set("Transfer-Encoding", "chunked") 212 | c.Writer.Header().Set("X-Accel-Buffering", "no") 213 | } 214 | -------------------------------------------------------------------------------- /common/envs.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "os" 5 | "time" 6 | ) 7 | 8 | var Version = "v0.0.0" 9 | var StartTime = time.Now().Unix() // unit: second 10 | 11 | var PProfEnabled = os.Getenv("PPROF") == "true" 12 | var DebugEnabled = os.Getenv("DEBUG") == "true" 13 | var LogDir = GetOrDefaultString("LOG_DIR", "./logs") 14 | var RotateLogs = os.Getenv("ROTATE_LOGS") == "true" 15 | 16 | var Port = GetOrDefaultString("PORT", "8000") 17 | var Proxy = GetOrDefaultString("PROXY", "") 18 | 19 | var BaseUrl = GetOrDefaultString("BASE_URL", "https://internal-api.virginia.labs.lumalabs.ai") 20 | var COOKIE = GetOrDefaultString("COOKIE", "") 21 | 22 | // var AccessToken = GetOrDefaultString("ACCESS_TOKEN", "") 23 | var SecretToken = GetOrDefaultString("SECRET_TOKEN", "") 24 | 25 | var ChatTemplateDir = GetOrDefaultString("CHAT_TEMPLATE_DIR", "./template") 26 | var ChatOpenaiModel = GetOrDefaultString("CHAT_OPENAI_MODEL", "gpt-4o") 27 | var ChatOpenaiApiBASE = GetOrDefaultString("CHAT_OPENAI_BASE", "https://api.openai.com") 28 | var ChatOpenaiApiKey = GetOrDefaultString("CHAT_OPENAI_KEY", "") 29 | var ChatTimeOut = GetOrDefault("CHAT_TIME_OUT", 600) // 任务超时时间 30 | -------------------------------------------------------------------------------- /common/log.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "sync" 9 | "time" 10 | 11 | rotatelogs "github.com/lestrrat-go/file-rotatelogs" 12 | "go.uber.org/zap" 13 | "go.uber.org/zap/zapcore" 14 | ) 15 | 16 | var Logger *zap.SugaredLogger 17 | var LoggerZap *zap.Logger 18 | var setupLogLock sync.Mutex 19 | 20 | func SetupLogger() { 21 | if Logger != nil { 22 | return 23 | } 24 | ok := setupLogLock.TryLock() 25 | if !ok { 26 | log.Println("setup log is already working") 27 | return 28 | } 29 | defer func() { 30 | setupLogLock.Unlock() 31 | }() 32 | levelEnabler := zapcore.DebugLevel 33 | syncers := []zapcore.WriteSyncer{zapcore.AddSync(os.Stdout)} 34 | 35 | if LogDir != "" { 36 | var err error 37 | LogDir, err = filepath.Abs(LogDir) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | if _, err := os.Stat(LogDir); os.IsNotExist(err) { 42 | err = os.Mkdir(LogDir, 0777) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | } 47 | 48 | if RotateLogs { 49 | fd, err := rotatelogs.New( 50 | filepath.Join(LogDir, "%Y%m%d", "%H:%M.log"), 51 | rotatelogs.WithRotationTime(time.Hour), 52 | rotatelogs.WithMaxAge(time.Hour*24*100), 53 | ) 54 | if err != nil { 55 | log.Fatal("failed to open rotateLogs") 56 | } 57 | syncers = append(syncers, zapcore.AddSync(fd)) 58 | } else { 59 | logPath := filepath.Join(LogDir, fmt.Sprintf("luma2api-%s.log", time.Now().Format("20060102"))) 60 | fd, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 61 | if err != nil { 62 | log.Fatal("failed to open log file") 63 | } 64 | syncers = append(syncers, zapcore.AddSync(fd)) 65 | } 66 | } 67 | enc := zapcore.NewJSONEncoder(zapcore.EncoderConfig{ 68 | TimeKey: "_time", // Modified 69 | LevelKey: "level", 70 | NameKey: "logger", 71 | CallerKey: "caller", 72 | //FunctionKey: zapcore.OmitKey, 73 | MessageKey: "msg", 74 | StacktraceKey: "stacktrace", 75 | LineEnding: zapcore.DefaultLineEnding, 76 | EncodeLevel: zapcore.LowercaseLevelEncoder, 77 | EncodeTime: zapcore.RFC3339NanoTimeEncoder, // Modified 78 | EncodeDuration: zapcore.SecondsDurationEncoder, 79 | EncodeCaller: zapcore.ShortCallerEncoder, 80 | }) 81 | 82 | loggerCore := zapcore.NewCore(enc, 83 | zapcore.NewMultiWriteSyncer(syncers...), 84 | 85 | levelEnabler) 86 | LoggerZap = zap.New(loggerCore, 87 | zap.AddStacktrace( 88 | zap.NewAtomicLevelAt(zapcore.ErrorLevel)), 89 | zap.AddCaller(), 90 | zap.AddCallerSkip(0), 91 | ) 92 | Logger = LoggerZap.Sugar() 93 | //gin.DefaultWriter = Logger.Writer() 94 | //gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, logsWriter) 95 | } 96 | -------------------------------------------------------------------------------- /common/openai_tools.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "github.com/sashabaranov/go-openai" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | type GeneralOpenAIRequest struct { 13 | Model string `json:"model,omitempty"` 14 | Messages []Message `json:"messages,omitempty"` 15 | Stream bool `json:"stream,omitempty"` 16 | MaxTokens uint `json:"max_tokens,omitempty"` 17 | Temperature float64 `json:"temperature,omitempty"` 18 | TopP float64 `json:"top_p,omitempty"` 19 | TopK int `json:"top_k,omitempty"` 20 | FunctionCall interface{} `json:"function_call,omitempty"` 21 | FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` 22 | PresencePenalty float64 `json:"presence_penalty,omitempty"` 23 | ToolChoice string `json:"tool_choice,omitempty"` 24 | Tools []Tool `json:"tools,omitempty"` 25 | } 26 | 27 | type Message struct { 28 | Role string `json:"role"` 29 | Content string `json:"content"` 30 | Name *string `json:"name,omitempty"` 31 | } 32 | 33 | type Function struct { 34 | Url string `json:"url,omitempty"` 35 | Name string `json:"name"` 36 | Description string `json:"description"` 37 | Parameters Parameter `json:"parameters"` 38 | Arguments string `json:"arguments,omitempty"` 39 | } 40 | type Parameter struct { 41 | Type string `json:"type"` 42 | Properties map[string]Property `json:"properties"` 43 | Required []string `json:"required"` 44 | } 45 | 46 | type Property struct { 47 | Type string `json:"type"` 48 | Description string `json:"description"` 49 | Enum []string `json:"enum,omitempty"` 50 | } 51 | 52 | type Tool struct { 53 | Id string `json:"id,omitempty"` 54 | Type string `json:"type"` 55 | Function Function `json:"function"` 56 | } 57 | 58 | type ChatCompletionsStreamResponse struct { 59 | Id string `json:"id"` 60 | Object string `json:"object"` 61 | Created interface{} `json:"created"` 62 | Model string `json:"model"` 63 | Choices []ChatCompletionsStreamResponseChoice `json:"choices"` 64 | } 65 | 66 | type ChatCompletionsStreamResponseChoice struct { 67 | Index int `json:"index"` 68 | Delta struct { 69 | Content string `json:"content"` 70 | Role string `json:"role,omitempty"` 71 | } `json:"delta"` 72 | FinishReason *string `json:"finish_reason,omitempty"` 73 | } 74 | 75 | type OpenAIError struct { 76 | Error Error `json:"error"` 77 | } 78 | 79 | type Error struct { 80 | Message string `json:"message"` 81 | Type string `json:"type"` 82 | Param string `json:"param"` 83 | Code any `json:"code"` 84 | } 85 | 86 | func ConstructChatCompletionStreamReponse(model, answerID string, answer string) openai.ChatCompletionStreamResponse { 87 | resp := openai.ChatCompletionStreamResponse{ 88 | ID: answerID, 89 | Object: "chat.completion.chunk", 90 | Created: time.Now().Unix(), 91 | Model: model, 92 | Choices: []openai.ChatCompletionStreamChoice{ 93 | { 94 | Index: 0, 95 | Delta: openai.ChatCompletionStreamChoiceDelta{ 96 | Content: answer, 97 | }, 98 | }, 99 | }, 100 | } 101 | return resp 102 | } 103 | 104 | func SendChatData(w http.ResponseWriter, model string, chatID, data string) { 105 | dataRune := []rune(data) 106 | for _, d := range dataRune { 107 | respData := ConstructChatCompletionStreamReponse(model, chatID, string(d)) 108 | byteData, _ := json.Marshal(respData) 109 | _, _ = fmt.Fprintf(w, "data: %s\n\n", string(byteData)) 110 | w.(http.Flusher).Flush() 111 | time.Sleep(1 * time.Millisecond) 112 | } 113 | } 114 | 115 | func SendChatDone(w http.ResponseWriter) { 116 | _, _ = fmt.Fprintf(w, "data: [DONE]") 117 | w.(http.Flusher).Flush() 118 | } 119 | 120 | func WrapperOpenAIError(err error, code string) *OpenAIError { 121 | return &OpenAIError{ 122 | Error: Error{ 123 | Message: err.Error(), 124 | Code: code, 125 | }, 126 | } 127 | } 128 | 129 | func ReturnOpenAIError(c *gin.Context, err error, code string, statusCode int) { 130 | c.JSON(statusCode, OpenAIError{ 131 | Error: Error{ 132 | Message: err.Error(), 133 | Code: code, 134 | }, 135 | }) 136 | return 137 | } 138 | -------------------------------------------------------------------------------- /common/template.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pkg/errors" 6 | "gopkg.in/yaml.v3" 7 | "html/template" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | var Templates map[string]*template.Template 14 | 15 | func InitTemplate() { 16 | // init 17 | // load template file 18 | Templates = make(map[string]*template.Template) 19 | // register template 20 | if ChatTemplateDir == "" { 21 | Logger.Warnf("InitTemplate empty TemplateDir") 22 | return 23 | } 24 | 25 | exist, err := PathExists(ChatTemplateDir) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | if !exist { 31 | Logger.Warnf("InitTemplate empty TemplateDir") 32 | return 33 | } 34 | 35 | err = filepath.Walk(ChatTemplateDir, func(path string, info os.FileInfo, err error) error { 36 | if info.IsDir() { 37 | return nil 38 | } 39 | if filepath.Ext(path) != ".yaml" { 40 | return nil 41 | } 42 | var tempContent map[string]string 43 | fileBody, err := os.ReadFile(path) 44 | if err != nil { 45 | err = errors.Wrap(err, fmt.Sprintf("load failed: %s", info.Name())) 46 | return err 47 | } 48 | err = yaml.Unmarshal(fileBody, &tempContent) 49 | if err != nil { 50 | err = errors.Wrap(err, fmt.Sprintf("load failed: %s", info.Name())) 51 | return err 52 | } 53 | 54 | for k, v := range tempContent { 55 | if strings.TrimSpace(v) == "" { 56 | continue 57 | } 58 | subTemp, err := template.New(info.Name() + k).Parse(v) 59 | if err != nil { 60 | return err 61 | } 62 | Templates[k] = subTemp 63 | } 64 | 65 | return nil 66 | }) 67 | if err != nil { 68 | panic(err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /common/utils.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/gin-gonic/gin" 8 | "github.com/google/uuid" 9 | "io" 10 | "math/rand" 11 | "os" 12 | "path/filepath" 13 | "runtime" 14 | "runtime/debug" 15 | "strconv" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | func SafeGoroutine(f func()) { 21 | go func() { 22 | defer func() { 23 | if r := recover(); r != nil { 24 | Logger.Error(fmt.Sprintf("child goroutine panic occured: error: %v, stack: %s", r, string(debug.Stack()))) 25 | } 26 | }() 27 | f() 28 | }() 29 | } 30 | 31 | func Any2String(inter interface{}) string { 32 | if inter == nil { 33 | return "" 34 | } 35 | switch inter.(type) { 36 | case string: 37 | return inter.(string) 38 | case *string: 39 | ptr, ok := inter.(*string) 40 | if !ok || ptr == nil { 41 | return "" 42 | } 43 | return *ptr 44 | case int, int64: 45 | return fmt.Sprintf("%d", inter) 46 | case float64: 47 | return fmt.Sprintf("%f", inter) 48 | } 49 | return "Not Implemented" 50 | } 51 | 52 | func Any2Int(data any) int { 53 | if data == nil { 54 | return 0 55 | } 56 | if valueAssert, ok := data.(float64); ok { 57 | return int(valueAssert) 58 | } 59 | if valueAssert, ok := data.(int64); ok { 60 | return int(valueAssert) 61 | } 62 | if valueAssert, ok := data.(int); ok { 63 | return valueAssert 64 | } 65 | if valueAssert, ok := data.(*int); ok { 66 | return *valueAssert 67 | } 68 | if valueAssert, ok := data.(string); ok { 69 | valueAssertInt, _ := strconv.Atoi(valueAssert) 70 | return valueAssertInt 71 | } 72 | return 0 73 | } 74 | 75 | func Any2Bool(data any) bool { 76 | if data == nil { 77 | return false 78 | } 79 | switch data.(type) { 80 | case string: 81 | if data == "true" { 82 | return true 83 | } 84 | if data == "1" { 85 | return true 86 | } 87 | return false 88 | case int: 89 | if data == 1 { 90 | return true 91 | } 92 | return false 93 | case bool: 94 | return data.(bool) 95 | case nil: 96 | return false 97 | } 98 | return false 99 | } 100 | 101 | func GetUUID() string { 102 | code := uuid.New().String() 103 | code = strings.Replace(code, "-", "", -1) 104 | return code 105 | } 106 | 107 | const keyChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 108 | 109 | func init() { 110 | rand.Seed(time.Now().UnixNano()) 111 | } 112 | 113 | func GetRandomString(length int) string { 114 | //rand.Seed(time.Now().UnixNano()) 115 | key := make([]byte, length) 116 | for i := 0; i < length; i++ { 117 | key[i] = keyChars[rand.Intn(len(keyChars))] 118 | } 119 | return string(key) 120 | } 121 | 122 | func GetTimeString() string { 123 | now := time.Now() 124 | return fmt.Sprintf("%s%d", now.Format("20060102150405"), now.UnixNano()%1e9) 125 | } 126 | 127 | func GetOrDefault(env string, defaultValue int) int { 128 | if env == "" || os.Getenv(env) == "" { 129 | return defaultValue 130 | } 131 | num, err := strconv.Atoi(os.Getenv(env)) 132 | if err != nil { 133 | Logger.Error(fmt.Sprintf("failed to parse %s: %s, using default value: %d", env, err.Error(), defaultValue)) 134 | return defaultValue 135 | } 136 | return num 137 | } 138 | 139 | func GetOrDefaultString(env string, defaultValue string) string { 140 | if env == "" || os.Getenv(env) == "" { 141 | return defaultValue 142 | } 143 | return os.Getenv(env) 144 | } 145 | 146 | func GetJsonString(data any) string { 147 | if data == nil { 148 | return "" 149 | } 150 | b, _ := json.Marshal(data) 151 | return string(b) 152 | } 153 | 154 | func UnmarshalBodyReusable(c *gin.Context, v any) error { 155 | requestBody, err := GetRequestBody(c) 156 | if err != nil { 157 | return err 158 | } 159 | contentType := c.Request.Header.Get("Content-Type") 160 | if strings.Contains(contentType, "application/json") { 161 | err = json.Unmarshal(requestBody, &v) 162 | } else { 163 | // skip for now 164 | // try 165 | err = json.Unmarshal(requestBody, &v) 166 | } 167 | if err != nil { 168 | return err 169 | } 170 | // Reset request body 171 | c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) 172 | return nil 173 | } 174 | 175 | const KeyRequestBody = "key_request_body" 176 | 177 | func GetRequestBody(c *gin.Context) ([]byte, error) { 178 | requestBody, _ := c.Get(KeyRequestBody) 179 | if requestBody != nil { 180 | return requestBody.([]byte), nil 181 | } 182 | requestBody, err := io.ReadAll(c.Request.Body) 183 | if err != nil { 184 | return nil, err 185 | } 186 | _ = c.Request.Body.Close() 187 | c.Set(KeyRequestBody, requestBody) 188 | return requestBody.([]byte), nil 189 | } 190 | 191 | func GetRootDir() string { 192 | _, filename, _, _ := runtime.Caller(0) 193 | utilDir := filepath.Dir(filename) 194 | paths, _ := filepath.Split(utilDir) 195 | return paths 196 | } 197 | 198 | func ToPtr[T any](arg T) *T { 199 | return &arg 200 | } 201 | 202 | func PathExists(path string) (bool, error) { 203 | _, err := os.Stat(path) 204 | if err == nil { 205 | return true, nil 206 | } 207 | if os.IsNotExist(err) { 208 | return false, nil 209 | } 210 | return false, err 211 | } 212 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | sunoapi: 5 | image: lumaapi/luma-api:latest 6 | container_name: luma-api 7 | restart: always 8 | ports: 9 | - "8000:8000" 10 | volumes: 11 | - ./logs:/logs 12 | environment: 13 | - COOKIE= 14 | 15 | -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // Code generated by swaggo/swag. DO NOT EDIT. 2 | 3 | package docs 4 | 5 | import "github.com/swaggo/swag" 6 | 7 | const docTemplate = `{ 8 | "schemes": {{ marshal .Schemes }}, 9 | "swagger": "2.0", 10 | "info": { 11 | "description": "{{escape .Description}}", 12 | "title": "{{.Title}}", 13 | "contact": {}, 14 | "version": "{{.Version}}" 15 | }, 16 | "host": "{{.Host}}", 17 | "basePath": "{{.BasePath}}", 18 | "paths": { 19 | "/luma/generations": { 20 | "post": { 21 | "consumes": [ 22 | "application/json" 23 | ], 24 | "produces": [ 25 | "application/json" 26 | ], 27 | "summary": "Submit luma generate video task", 28 | "parameters": [ 29 | { 30 | "description": "submit generate video", 31 | "name": "body", 32 | "in": "body", 33 | "required": true, 34 | "schema": { 35 | "$ref": "#/definitions/main.GenRequest" 36 | } 37 | } 38 | ], 39 | "responses": { 40 | "200": { 41 | "description": "generate result", 42 | "schema": { 43 | "type": "array", 44 | "items": { 45 | "$ref": "#/definitions/main.VideoTask" 46 | } 47 | } 48 | } 49 | } 50 | } 51 | }, 52 | "/luma/generations/": { 53 | "get": { 54 | "consumes": [ 55 | "application/json" 56 | ], 57 | "produces": [ 58 | "application/json" 59 | ], 60 | "summary": "Get luma generate video task", 61 | "parameters": [ 62 | { 63 | "type": "string", 64 | "description": "page offset", 65 | "name": "offset", 66 | "in": "query", 67 | "required": true 68 | }, 69 | { 70 | "type": "string", 71 | "description": "page limit", 72 | "name": "limit", 73 | "in": "query", 74 | "required": true 75 | } 76 | ], 77 | "responses": { 78 | "200": { 79 | "description": "video tasks", 80 | "schema": { 81 | "type": "array", 82 | "items": { 83 | "$ref": "#/definitions/main.VideoTask" 84 | } 85 | } 86 | } 87 | } 88 | } 89 | }, 90 | "/luma/generations/:task_id/extend": { 91 | "post": { 92 | "consumes": [ 93 | "application/json" 94 | ], 95 | "produces": [ 96 | "application/json" 97 | ], 98 | "summary": "Submit luma extend generate video task", 99 | "parameters": [ 100 | { 101 | "type": "string", 102 | "description": "extend task id", 103 | "name": "task_id", 104 | "in": "path", 105 | "required": true 106 | }, 107 | { 108 | "description": "submit generate video", 109 | "name": "body", 110 | "in": "body", 111 | "required": true, 112 | "schema": { 113 | "$ref": "#/definitions/main.GenRequest" 114 | } 115 | } 116 | ], 117 | "responses": { 118 | "200": { 119 | "description": "generate result", 120 | "schema": { 121 | "type": "array", 122 | "items": { 123 | "$ref": "#/definitions/main.VideoTask" 124 | } 125 | } 126 | } 127 | } 128 | } 129 | }, 130 | "/luma/generations/file_upload": { 131 | "post": { 132 | "consumes": [ 133 | "application/json" 134 | ], 135 | "produces": [ 136 | "application/json" 137 | ], 138 | "summary": "Upload image to luma", 139 | "parameters": [ 140 | { 141 | "description": "Upload image params", 142 | "name": "body", 143 | "in": "body", 144 | "required": true, 145 | "schema": { 146 | "$ref": "#/definitions/main.UploadReq" 147 | } 148 | } 149 | ], 150 | "responses": { 151 | "200": { 152 | "description": "upload result", 153 | "schema": { 154 | "type": "array", 155 | "items": { 156 | "$ref": "#/definitions/main.FileUploadResult" 157 | } 158 | } 159 | } 160 | } 161 | } 162 | }, 163 | "/luma/generations/{task_id}": { 164 | "get": { 165 | "consumes": [ 166 | "application/json" 167 | ], 168 | "produces": [ 169 | "application/json" 170 | ], 171 | "summary": "Get luma generate video task", 172 | "parameters": [ 173 | { 174 | "type": "string", 175 | "description": "fetch single task by id", 176 | "name": "task_id", 177 | "in": "path", 178 | "required": true 179 | } 180 | ], 181 | "responses": { 182 | "200": { 183 | "description": "video single task", 184 | "schema": { 185 | "$ref": "#/definitions/main.VideoTask" 186 | } 187 | } 188 | } 189 | } 190 | }, 191 | "/luma/generations/{task_id}/download_video_url": { 192 | "post": { 193 | "consumes": [ 194 | "application/json" 195 | ], 196 | "produces": [ 197 | "application/json" 198 | ], 199 | "summary": "Get video url without watermark", 200 | "parameters": [ 201 | { 202 | "type": "string", 203 | "description": "fetch by id", 204 | "name": "task_id", 205 | "in": "path", 206 | "required": true 207 | } 208 | ], 209 | "responses": { 210 | "200": { 211 | "description": "url", 212 | "schema": { 213 | "type": "object" 214 | } 215 | } 216 | } 217 | } 218 | }, 219 | "/luma/subscription/usage": { 220 | "get": { 221 | "consumes": [ 222 | "application/json" 223 | ], 224 | "produces": [ 225 | "application/json" 226 | ], 227 | "summary": "Get current user subscription usage", 228 | "responses": { 229 | "200": { 230 | "description": "subscription info", 231 | "schema": { 232 | "type": "object" 233 | } 234 | } 235 | } 236 | } 237 | }, 238 | "/luma/users/me": { 239 | "get": { 240 | "consumes": [ 241 | "application/json" 242 | ], 243 | "produces": [ 244 | "application/json" 245 | ], 246 | "summary": "Get current user info", 247 | "responses": { 248 | "200": { 249 | "description": "user info", 250 | "schema": { 251 | "type": "object" 252 | } 253 | } 254 | } 255 | } 256 | } 257 | }, 258 | "definitions": { 259 | "main.FileUploadResult": { 260 | "type": "object", 261 | "properties": { 262 | "id": { 263 | "type": "string" 264 | }, 265 | "presigned_url": { 266 | "type": "string" 267 | }, 268 | "public_url": { 269 | "type": "string" 270 | } 271 | } 272 | }, 273 | "main.GenRequest": { 274 | "type": "object", 275 | "properties": { 276 | "aspect_ratio": { 277 | "description": "option", 278 | "type": "string" 279 | }, 280 | "expand_prompt": { 281 | "description": "option", 282 | "type": "boolean" 283 | }, 284 | "image_end_url": { 285 | "description": "option, uploaded refer image url", 286 | "type": "string" 287 | }, 288 | "image_url": { 289 | "description": "option, uploaded refer image url", 290 | "type": "string" 291 | }, 292 | "user_prompt": { 293 | "description": "option", 294 | "type": "string" 295 | } 296 | } 297 | }, 298 | "main.UploadReq": { 299 | "type": "object", 300 | "properties": { 301 | "url": { 302 | "description": "support public url \u0026 base64", 303 | "type": "string" 304 | } 305 | } 306 | }, 307 | "main.Video": { 308 | "type": "object", 309 | "properties": { 310 | "height": { 311 | "type": "integer" 312 | }, 313 | "thumbnail": {}, 314 | "url": { 315 | "type": "string" 316 | }, 317 | "width": { 318 | "type": "integer" 319 | } 320 | } 321 | }, 322 | "main.VideoTask": { 323 | "type": "object", 324 | "properties": { 325 | "created_at": { 326 | "type": "string" 327 | }, 328 | "estimate_wait_seconds": {}, 329 | "id": { 330 | "type": "string" 331 | }, 332 | "liked": {}, 333 | "prompt": { 334 | "type": "string" 335 | }, 336 | "state": { 337 | "description": "\"pending\", \"processing\", \"completed\"", 338 | "type": "string" 339 | }, 340 | "video": { 341 | "$ref": "#/definitions/main.Video" 342 | } 343 | } 344 | } 345 | } 346 | }` 347 | 348 | // SwaggerInfo holds exported Swagger Info so clients can modify it 349 | var SwaggerInfo = &swag.Spec{ 350 | Version: "", 351 | Host: "", 352 | BasePath: "", 353 | Schemes: []string{}, 354 | Title: "", 355 | Description: "", 356 | InfoInstanceName: "swagger", 357 | SwaggerTemplate: docTemplate, 358 | LeftDelim: "{{", 359 | RightDelim: "}}", 360 | } 361 | 362 | func init() { 363 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 364 | } 365 | -------------------------------------------------------------------------------- /docs/images/cookie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumaAI-API/Luma-API/291c41f172613cab280675aede8b152ebd7a102a/docs/images/cookie.jpg -------------------------------------------------------------------------------- /docs/images/token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumaAI-API/Luma-API/291c41f172613cab280675aede8b152ebd7a102a/docs/images/token.png -------------------------------------------------------------------------------- /docs/images/zanshangcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumaAI-API/Luma-API/291c41f172613cab280675aede8b152ebd7a102a/docs/images/zanshangcode.jpg -------------------------------------------------------------------------------- /docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "contact": {} 5 | }, 6 | "paths": { 7 | "/luma/generations": { 8 | "post": { 9 | "consumes": [ 10 | "application/json" 11 | ], 12 | "produces": [ 13 | "application/json" 14 | ], 15 | "summary": "Submit luma generate video task", 16 | "parameters": [ 17 | { 18 | "description": "submit generate video", 19 | "name": "body", 20 | "in": "body", 21 | "required": true, 22 | "schema": { 23 | "$ref": "#/definitions/main.GenRequest" 24 | } 25 | } 26 | ], 27 | "responses": { 28 | "200": { 29 | "description": "generate result", 30 | "schema": { 31 | "type": "array", 32 | "items": { 33 | "$ref": "#/definitions/main.VideoTask" 34 | } 35 | } 36 | } 37 | } 38 | } 39 | }, 40 | "/luma/generations/": { 41 | "get": { 42 | "consumes": [ 43 | "application/json" 44 | ], 45 | "produces": [ 46 | "application/json" 47 | ], 48 | "summary": "Get luma generate video task", 49 | "parameters": [ 50 | { 51 | "type": "string", 52 | "description": "page offset", 53 | "name": "offset", 54 | "in": "query", 55 | "required": true 56 | }, 57 | { 58 | "type": "string", 59 | "description": "page limit", 60 | "name": "limit", 61 | "in": "query", 62 | "required": true 63 | } 64 | ], 65 | "responses": { 66 | "200": { 67 | "description": "video tasks", 68 | "schema": { 69 | "type": "array", 70 | "items": { 71 | "$ref": "#/definitions/main.VideoTask" 72 | } 73 | } 74 | } 75 | } 76 | } 77 | }, 78 | "/luma/generations/:task_id/extend": { 79 | "post": { 80 | "consumes": [ 81 | "application/json" 82 | ], 83 | "produces": [ 84 | "application/json" 85 | ], 86 | "summary": "Submit luma extend generate video task", 87 | "parameters": [ 88 | { 89 | "type": "string", 90 | "description": "extend task id", 91 | "name": "task_id", 92 | "in": "path", 93 | "required": true 94 | }, 95 | { 96 | "description": "submit generate video", 97 | "name": "body", 98 | "in": "body", 99 | "required": true, 100 | "schema": { 101 | "$ref": "#/definitions/main.GenRequest" 102 | } 103 | } 104 | ], 105 | "responses": { 106 | "200": { 107 | "description": "generate result", 108 | "schema": { 109 | "type": "array", 110 | "items": { 111 | "$ref": "#/definitions/main.VideoTask" 112 | } 113 | } 114 | } 115 | } 116 | } 117 | }, 118 | "/luma/generations/file_upload": { 119 | "post": { 120 | "consumes": [ 121 | "application/json" 122 | ], 123 | "produces": [ 124 | "application/json" 125 | ], 126 | "summary": "Upload image to luma", 127 | "parameters": [ 128 | { 129 | "description": "Upload image params", 130 | "name": "body", 131 | "in": "body", 132 | "required": true, 133 | "schema": { 134 | "$ref": "#/definitions/main.UploadReq" 135 | } 136 | } 137 | ], 138 | "responses": { 139 | "200": { 140 | "description": "upload result", 141 | "schema": { 142 | "type": "array", 143 | "items": { 144 | "$ref": "#/definitions/main.FileUploadResult" 145 | } 146 | } 147 | } 148 | } 149 | } 150 | }, 151 | "/luma/generations/{task_id}": { 152 | "get": { 153 | "consumes": [ 154 | "application/json" 155 | ], 156 | "produces": [ 157 | "application/json" 158 | ], 159 | "summary": "Get luma generate video task", 160 | "parameters": [ 161 | { 162 | "type": "string", 163 | "description": "fetch single task by id", 164 | "name": "task_id", 165 | "in": "path", 166 | "required": true 167 | } 168 | ], 169 | "responses": { 170 | "200": { 171 | "description": "video single task", 172 | "schema": { 173 | "$ref": "#/definitions/main.VideoTask" 174 | } 175 | } 176 | } 177 | } 178 | }, 179 | "/luma/generations/{task_id}/download_video_url": { 180 | "post": { 181 | "consumes": [ 182 | "application/json" 183 | ], 184 | "produces": [ 185 | "application/json" 186 | ], 187 | "summary": "Get video url without watermark", 188 | "parameters": [ 189 | { 190 | "type": "string", 191 | "description": "fetch by id", 192 | "name": "task_id", 193 | "in": "path", 194 | "required": true 195 | } 196 | ], 197 | "responses": { 198 | "200": { 199 | "description": "url", 200 | "schema": { 201 | "type": "object" 202 | } 203 | } 204 | } 205 | } 206 | }, 207 | "/luma/subscription/usage": { 208 | "get": { 209 | "consumes": [ 210 | "application/json" 211 | ], 212 | "produces": [ 213 | "application/json" 214 | ], 215 | "summary": "Get current user subscription usage", 216 | "responses": { 217 | "200": { 218 | "description": "subscription info", 219 | "schema": { 220 | "type": "object" 221 | } 222 | } 223 | } 224 | } 225 | }, 226 | "/luma/users/me": { 227 | "get": { 228 | "consumes": [ 229 | "application/json" 230 | ], 231 | "produces": [ 232 | "application/json" 233 | ], 234 | "summary": "Get current user info", 235 | "responses": { 236 | "200": { 237 | "description": "user info", 238 | "schema": { 239 | "type": "object" 240 | } 241 | } 242 | } 243 | } 244 | } 245 | }, 246 | "definitions": { 247 | "main.FileUploadResult": { 248 | "type": "object", 249 | "properties": { 250 | "id": { 251 | "type": "string" 252 | }, 253 | "presigned_url": { 254 | "type": "string" 255 | }, 256 | "public_url": { 257 | "type": "string" 258 | } 259 | } 260 | }, 261 | "main.GenRequest": { 262 | "type": "object", 263 | "properties": { 264 | "aspect_ratio": { 265 | "description": "option", 266 | "type": "string" 267 | }, 268 | "expand_prompt": { 269 | "description": "option", 270 | "type": "boolean" 271 | }, 272 | "image_end_url": { 273 | "description": "option, uploaded refer image url", 274 | "type": "string" 275 | }, 276 | "image_url": { 277 | "description": "option, uploaded refer image url", 278 | "type": "string" 279 | }, 280 | "user_prompt": { 281 | "description": "option", 282 | "type": "string" 283 | } 284 | } 285 | }, 286 | "main.UploadReq": { 287 | "type": "object", 288 | "properties": { 289 | "url": { 290 | "description": "support public url \u0026 base64", 291 | "type": "string" 292 | } 293 | } 294 | }, 295 | "main.Video": { 296 | "type": "object", 297 | "properties": { 298 | "height": { 299 | "type": "integer" 300 | }, 301 | "thumbnail": {}, 302 | "url": { 303 | "type": "string" 304 | }, 305 | "width": { 306 | "type": "integer" 307 | } 308 | } 309 | }, 310 | "main.VideoTask": { 311 | "type": "object", 312 | "properties": { 313 | "created_at": { 314 | "type": "string" 315 | }, 316 | "estimate_wait_seconds": {}, 317 | "id": { 318 | "type": "string" 319 | }, 320 | "liked": {}, 321 | "prompt": { 322 | "type": "string" 323 | }, 324 | "state": { 325 | "description": "\"pending\", \"processing\", \"completed\"", 326 | "type": "string" 327 | }, 328 | "video": { 329 | "$ref": "#/definitions/main.Video" 330 | } 331 | } 332 | } 333 | } 334 | } -------------------------------------------------------------------------------- /docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | main.FileUploadResult: 3 | properties: 4 | id: 5 | type: string 6 | presigned_url: 7 | type: string 8 | public_url: 9 | type: string 10 | type: object 11 | main.GenRequest: 12 | properties: 13 | aspect_ratio: 14 | description: option 15 | type: string 16 | expand_prompt: 17 | description: option 18 | type: boolean 19 | image_end_url: 20 | description: option, uploaded refer image url 21 | type: string 22 | image_url: 23 | description: option, uploaded refer image url 24 | type: string 25 | user_prompt: 26 | description: option 27 | type: string 28 | type: object 29 | main.UploadReq: 30 | properties: 31 | url: 32 | description: support public url & base64 33 | type: string 34 | type: object 35 | main.Video: 36 | properties: 37 | height: 38 | type: integer 39 | thumbnail: {} 40 | url: 41 | type: string 42 | width: 43 | type: integer 44 | type: object 45 | main.VideoTask: 46 | properties: 47 | created_at: 48 | type: string 49 | estimate_wait_seconds: {} 50 | id: 51 | type: string 52 | liked: {} 53 | prompt: 54 | type: string 55 | state: 56 | description: '"pending", "processing", "completed"' 57 | type: string 58 | video: 59 | $ref: '#/definitions/main.Video' 60 | type: object 61 | info: 62 | contact: {} 63 | paths: 64 | /luma/generations: 65 | post: 66 | consumes: 67 | - application/json 68 | parameters: 69 | - description: submit generate video 70 | in: body 71 | name: body 72 | required: true 73 | schema: 74 | $ref: '#/definitions/main.GenRequest' 75 | produces: 76 | - application/json 77 | responses: 78 | "200": 79 | description: generate result 80 | schema: 81 | items: 82 | $ref: '#/definitions/main.VideoTask' 83 | type: array 84 | summary: Submit luma generate video task 85 | /luma/generations/: 86 | get: 87 | consumes: 88 | - application/json 89 | parameters: 90 | - description: page offset 91 | in: query 92 | name: offset 93 | required: true 94 | type: string 95 | - description: page limit 96 | in: query 97 | name: limit 98 | required: true 99 | type: string 100 | produces: 101 | - application/json 102 | responses: 103 | "200": 104 | description: video tasks 105 | schema: 106 | items: 107 | $ref: '#/definitions/main.VideoTask' 108 | type: array 109 | summary: Get luma generate video task 110 | /luma/generations/:task_id/extend: 111 | post: 112 | consumes: 113 | - application/json 114 | parameters: 115 | - description: extend task id 116 | in: path 117 | name: task_id 118 | required: true 119 | type: string 120 | - description: submit generate video 121 | in: body 122 | name: body 123 | required: true 124 | schema: 125 | $ref: '#/definitions/main.GenRequest' 126 | produces: 127 | - application/json 128 | responses: 129 | "200": 130 | description: generate result 131 | schema: 132 | items: 133 | $ref: '#/definitions/main.VideoTask' 134 | type: array 135 | summary: Submit luma extend generate video task 136 | /luma/generations/{task_id}: 137 | get: 138 | consumes: 139 | - application/json 140 | parameters: 141 | - description: fetch single task by id 142 | in: path 143 | name: task_id 144 | required: true 145 | type: string 146 | produces: 147 | - application/json 148 | responses: 149 | "200": 150 | description: video single task 151 | schema: 152 | $ref: '#/definitions/main.VideoTask' 153 | summary: Get luma generate video task 154 | /luma/generations/{task_id}/download_video_url: 155 | post: 156 | consumes: 157 | - application/json 158 | parameters: 159 | - description: fetch by id 160 | in: path 161 | name: task_id 162 | required: true 163 | type: string 164 | produces: 165 | - application/json 166 | responses: 167 | "200": 168 | description: url 169 | schema: 170 | type: object 171 | summary: Get video url without watermark 172 | /luma/generations/file_upload: 173 | post: 174 | consumes: 175 | - application/json 176 | parameters: 177 | - description: Upload image params 178 | in: body 179 | name: body 180 | required: true 181 | schema: 182 | $ref: '#/definitions/main.UploadReq' 183 | produces: 184 | - application/json 185 | responses: 186 | "200": 187 | description: upload result 188 | schema: 189 | items: 190 | $ref: '#/definitions/main.FileUploadResult' 191 | type: array 192 | summary: Upload image to luma 193 | /luma/subscription/usage: 194 | get: 195 | consumes: 196 | - application/json 197 | produces: 198 | - application/json 199 | responses: 200 | "200": 201 | description: subscription info 202 | schema: 203 | type: object 204 | summary: Get current user subscription usage 205 | /luma/users/me: 206 | get: 207 | consumes: 208 | - application/json 209 | produces: 210 | - application/json 211 | responses: 212 | "200": 213 | description: user info 214 | schema: 215 | type: object 216 | summary: Get current user info 217 | swagger: "2.0" 218 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module luma-api 2 | 3 | go 1.21.6 4 | 5 | require ( 6 | github.com/bogdanfinn/tls-client v1.7.5 7 | github.com/gin-contrib/zap v1.1.3 8 | github.com/gin-gonic/gin v1.10.0 9 | github.com/google/uuid v1.6.0 10 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible 11 | github.com/pkg/errors v0.9.1 12 | github.com/sashabaranov/go-openai v1.25.0 13 | github.com/swaggo/files v1.0.1 14 | github.com/swaggo/gin-swagger v1.6.0 15 | github.com/swaggo/swag v1.16.3 16 | go.uber.org/zap v1.27.0 17 | gopkg.in/yaml.v3 v3.0.1 18 | ) 19 | 20 | require ( 21 | github.com/KyleBanks/depth v1.2.1 // indirect 22 | github.com/PuerkitoBio/purell v1.1.1 // indirect 23 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 24 | github.com/andybalholm/brotli v1.0.5 // indirect 25 | github.com/bogdanfinn/fhttp v0.5.28 // indirect 26 | github.com/bogdanfinn/utls v1.6.1 // indirect 27 | github.com/bytedance/sonic v1.11.6 // indirect 28 | github.com/bytedance/sonic/loader v0.1.1 // indirect 29 | github.com/cloudflare/circl v1.3.6 // indirect 30 | github.com/cloudwego/base64x v0.1.4 // indirect 31 | github.com/cloudwego/iasm v0.2.0 // indirect 32 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 33 | github.com/gin-contrib/sse v0.1.0 // indirect 34 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 35 | github.com/go-openapi/jsonreference v0.19.6 // indirect 36 | github.com/go-openapi/spec v0.20.4 // indirect 37 | github.com/go-openapi/swag v0.19.15 // indirect 38 | github.com/go-playground/locales v0.14.1 // indirect 39 | github.com/go-playground/universal-translator v0.18.1 // indirect 40 | github.com/go-playground/validator/v10 v10.20.0 // indirect 41 | github.com/goccy/go-json v0.10.2 // indirect 42 | github.com/jonboulle/clockwork v0.4.0 // indirect 43 | github.com/josharian/intern v1.0.0 // indirect 44 | github.com/json-iterator/go v1.1.12 // indirect 45 | github.com/klauspost/compress v1.16.7 // indirect 46 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 47 | github.com/leodido/go-urn v1.4.0 // indirect 48 | github.com/lestrrat-go/strftime v1.0.6 // indirect 49 | github.com/mailru/easyjson v0.7.6 // indirect 50 | github.com/mattn/go-isatty v0.0.20 // indirect 51 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 52 | github.com/modern-go/reflect2 v1.0.2 // indirect 53 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 54 | github.com/quic-go/quic-go v0.37.4 // indirect 55 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect 56 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 57 | github.com/ugorji/go/codec v1.2.12 // indirect 58 | go.uber.org/multierr v1.11.0 // indirect 59 | golang.org/x/arch v0.8.0 // indirect 60 | golang.org/x/crypto v0.23.0 // indirect 61 | golang.org/x/net v0.25.0 // indirect 62 | golang.org/x/sys v0.20.0 // indirect 63 | golang.org/x/text v0.15.0 // indirect 64 | golang.org/x/tools v0.9.1 // indirect 65 | google.golang.org/protobuf v1.34.1 // indirect 66 | gopkg.in/yaml.v2 v2.4.0 // indirect 67 | ) 68 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 2 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 3 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= 4 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 5 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 6 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 7 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 8 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 9 | github.com/bogdanfinn/fhttp v0.5.28 h1:G6thT8s8v6z1IuvXMUsX9QKy3ZHseTQTzxuIhSiaaAw= 10 | github.com/bogdanfinn/fhttp v0.5.28/go.mod h1:oJiYPG3jQTKzk/VFmogH8jxjH5yiv2rrOH48Xso2lrE= 11 | github.com/bogdanfinn/tls-client v1.7.5 h1:R1aTwe5oja5niLnQggzbWnzJEssw9n+3O4kR0H/Tjl4= 12 | github.com/bogdanfinn/tls-client v1.7.5/go.mod h1:pQwF0eqfL0gf0mu8hikvu6deZ3ijSPruJDzEKEnnXjU= 13 | github.com/bogdanfinn/utls v1.6.1 h1:dKDYAcXEyFFJ3GaWaN89DEyjyRraD1qb4osdEK89ass= 14 | github.com/bogdanfinn/utls v1.6.1/go.mod h1:VXIbRZaiY/wHZc6Hu+DZ4O2CgTzjhjCg/Ou3V4r/39Y= 15 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= 16 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 17 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 18 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 19 | github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg= 20 | github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 21 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 22 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 23 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 24 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 25 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 26 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 28 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 30 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 31 | github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= 32 | github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= 33 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 34 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 35 | github.com/gin-contrib/zap v1.1.3 h1:9e/U9fYd4/OBfmSEBs5hHZq114uACn7bpuzvCkcJySA= 36 | github.com/gin-contrib/zap v1.1.3/go.mod h1:+BD/6NYZKJyUpqVoJEvgeq9GLz8pINEQvak9LHNOTSE= 37 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 38 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 39 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 40 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 41 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 42 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= 43 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 44 | github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= 45 | github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= 46 | github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= 47 | github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= 48 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 49 | github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= 50 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 51 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 52 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 53 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 54 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 55 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 56 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 57 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= 58 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 59 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 60 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 61 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 62 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 63 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 64 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 65 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 66 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= 67 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 68 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 69 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 70 | github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= 71 | github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= 72 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 73 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 74 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 75 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 76 | github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= 77 | github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 78 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 79 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 80 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 81 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 82 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 83 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 84 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 85 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 86 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 87 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 88 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 89 | github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= 90 | github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= 91 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4= 92 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= 93 | github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ= 94 | github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw= 95 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 96 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 97 | github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= 98 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 99 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 100 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 101 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 102 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 103 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 104 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 105 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 106 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 107 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 108 | github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= 109 | github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= 110 | github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= 111 | github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= 112 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 113 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 114 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 115 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 116 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 117 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 118 | github.com/quic-go/quic-go v0.37.4 h1:ke8B73yMCWGq9MfrCCAw0Uzdm7GaViC3i39dsIdDlH4= 119 | github.com/quic-go/quic-go v0.37.4/go.mod h1:YsbH1r4mSHPJcLF4k4zruUkLBqctEMBDR6VPvcYjIsU= 120 | github.com/sashabaranov/go-openai v1.25.0 h1:3h3DtJ55zQJqc+BR4y/iTcPhLk4pewJpyO+MXW2RdW0= 121 | github.com/sashabaranov/go-openai v1.25.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 122 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 123 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 124 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 125 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 126 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 127 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 128 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 129 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 130 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 131 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 132 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 133 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 134 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 135 | github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= 136 | github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= 137 | github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= 138 | github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= 139 | github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= 140 | github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= 141 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc= 142 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng= 143 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 144 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 145 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 146 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 147 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 148 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 149 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 150 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 151 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 152 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 153 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 154 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 155 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 156 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 157 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 158 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 159 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= 160 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 161 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 162 | golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= 163 | golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 164 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 165 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 166 | golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 167 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 168 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 169 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 170 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 171 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 172 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 173 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 174 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 175 | golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 176 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 177 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 178 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 179 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 180 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 181 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 182 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 183 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 184 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 185 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 186 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 187 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 188 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 189 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 190 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 191 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= 192 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 193 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 194 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 195 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 196 | golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= 197 | golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= 198 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 199 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 200 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 201 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 202 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 203 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 204 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 205 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 206 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 207 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 208 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 209 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 210 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 211 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 212 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 213 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 214 | -------------------------------------------------------------------------------- /keep-alive.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "luma-api/common" 5 | "time" 6 | ) 7 | 8 | func StartAllKeepAlive() { 9 | for { 10 | time.Sleep(10 * time.Second) 11 | if GetLumaAuth() == "" { 12 | continue 13 | } 14 | _, err := getMe() 15 | if err != nil { 16 | common.Logger.Errorf("Luma Keep-alive 失败, err: %s", err.Error()) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Luma-API 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /logs/luma2api-20240618.log: -------------------------------------------------------------------------------- 1 | {"level":"info","_time":"2024-06-18T22:30:20.1632713+08:00","caller":"Luma-API/main.go:15","msg":"Luma-API started"} 2 | {"level":"info","_time":"2024-06-18T22:30:45.6636795+08:00","caller":"Luma-API/main.go:15","msg":"Luma-API started"} 3 | {"level":"info","_time":"2024-06-18T22:30:45.6641947+08:00","caller":"Luma-API/main.go:45","msg":"Start: 0.0.0.0:8000"} 4 | {"level":"info","_time":"2024-06-18T22:38:22.1361724+08:00","caller":"Luma-API/main.go:15","msg":"Luma-API started"} 5 | {"level":"info","_time":"2024-06-18T22:38:22.1366849+08:00","caller":"Luma-API/main.go:45","msg":"Start: 0.0.0.0:8000"} 6 | {"level":"info","_time":"2024-06-18T22:45:13.4361659+08:00","caller":"Luma-API/main.go:15","msg":"Luma-API started"} 7 | {"level":"info","_time":"2024-06-18T22:45:13.4438757+08:00","caller":"Luma-API/main.go:45","msg":"Start: 0.0.0.0:8000"} 8 | {"level":"info","_time":"2024-06-18T22:47:02.0242619+08:00","caller":"Luma-API/main.go:15","msg":"Luma-API started"} 9 | {"level":"info","_time":"2024-06-18T22:47:02.0252934+08:00","caller":"Luma-API/main.go:45","msg":"Start: 0.0.0.0:8000"} 10 | {"level":"info","_time":"2024-06-18T22:47:35.2143966+08:00","caller":"Luma-API/api.go:51","msg":"upload file success","uploadRes":{"id":"3f0916d4-4541-464a-9fed-aac890c08b54","presigned_url":"https://dc53a917a6f427706a8ca3ecc7d70ea4.r2.cloudflarestorage.com/ai-lumalabs-storage/app_data/photon/user_uploads/3893d0d9-218c-4baa-b6ba-363dcf7ab1ec/3f0916d4-4541-464a-9fed-aac890c08b54_image_file.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=60bf44f30d45b472f9dd032de33e15d1%2F20240618%2Fauto%2Fs3%2Faws4_request&X-Amz-Date=20240618T144705Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=b3e3c6c0b53323950c97d470fcc10fedff3327e2953f98c4fba807712ef4addf","public_url":"https://storage.cdn-luma.com/app_data/photon/user_uploads/3893d0d9-218c-4baa-b6ba-363dcf7ab1ec/3f0916d4-4541-464a-9fed-aac890c08b54_image_file.jpg"}} 11 | -------------------------------------------------------------------------------- /luma-api: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumaAI-API/Luma-API/291c41f172613cab280675aede8b152ebd7a102a/luma-api -------------------------------------------------------------------------------- /luma.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "luma-api/common" 10 | "net/http" 11 | "regexp" 12 | "strings" 13 | "sync" 14 | 15 | "github.com/gin-gonic/gin" 16 | "github.com/pkg/errors" 17 | ) 18 | 19 | const ( 20 | SubmitEndpoint = "/api/photon/v1/generations/" 21 | GetTaskEndpoint = "/api/photon/v1/generations%s" 22 | FileUploadEndpoint = "/api/photon/v1/generations/file_upload" 23 | UsageEndpoint = "/api/photon/v1/subscription/usage" 24 | UserinfoEndpoint = "/api/users/v1/me" 25 | DownloadEndpoint = "/api/photon/v1/generations/%s/download_video_url" 26 | ) 27 | 28 | var CommonHeaders = map[string]string{ 29 | "Content-Type": "application/json", 30 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", 31 | "Referer": "https://lumalabs.ai/", 32 | "Origin": "https://lumalabs.ai", 33 | "Accept": "*/*", 34 | } 35 | 36 | func WrapperLumaError(c *gin.Context, err error, statusCode int) { 37 | common.Logger.Errorw("wrapper luma error", "statusCode", statusCode, "err", err) 38 | c.JSON(statusCode, gin.H{ 39 | "detail": map[string]any{ 40 | "reason": err.Error(), 41 | "code": 1, 42 | }, 43 | }) 44 | } 45 | 46 | func GenLumaError(err error, statusCode int) *WrapperErrorResp { 47 | return &WrapperErrorResp{ 48 | StatusCode: statusCode, 49 | ErrorResp: ErrorResp{ 50 | Detail: err.Error(), 51 | }, 52 | } 53 | } 54 | 55 | func ReturnLumaError(c *gin.Context, err ErrorResp, statusCode int) { 56 | common.Logger.Errorw("wrapper luma error", "statusCode", statusCode, "err", err) 57 | c.JSON(statusCode, err) 58 | } 59 | 60 | func GetLumaAuth() string { 61 | return common.COOKIE 62 | } 63 | 64 | var mu sync.Mutex 65 | 66 | func HandleRespCookies(resp *http.Response) { 67 | setCookies := resp.Header.Values("Set-Cookie") 68 | if len(setCookies) == 0 { 69 | return 70 | } 71 | mu.Lock() 72 | defer mu.Unlock() 73 | 74 | common.Logger.Infow("Luma账号触发返回cookie") 75 | 76 | //get old cookies 77 | cookies := make(map[string]string) 78 | for _, cookie := range strings.Split(common.COOKIE, ";") { 79 | kv := strings.Split(cookie, "=") 80 | if len(kv) == 2 { 81 | cookies[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1]) 82 | } 83 | } 84 | for _, cookie := range setCookies { 85 | names := strings.Split(cookie, "; ") 86 | kv := strings.Split(names[0], "=") 87 | if len(kv) == 2 { 88 | cookies[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1]) 89 | } 90 | } 91 | var cookieArr []string 92 | for k, v := range cookies { 93 | cookieArr = append(cookieArr, k+"="+v) 94 | } 95 | common.COOKIE = strings.Join(cookieArr, "; ") 96 | return 97 | } 98 | 99 | func doGeneration(c *gin.Context) { 100 | var genRequest GenRequest 101 | err := c.BindJSON(&genRequest) 102 | if err != nil { 103 | WrapperLumaError(c, err, http.StatusBadRequest) 104 | return 105 | } 106 | if genRequest.ImageUrl != "" && !strings.HasPrefix(genRequest.ImageUrl, "https://storage.cdn-luma.com/app_data/photon") { 107 | uploadRes, relayErr := uploadFile(genRequest.ImageUrl) 108 | if relayErr != nil { 109 | ReturnLumaError(c, relayErr.ErrorResp, relayErr.StatusCode) 110 | return 111 | } 112 | common.Logger.Infow("upload file success", "uploadRes", uploadRes) 113 | genRequest.ImageUrl = uploadRes.PublicUrl 114 | } 115 | 116 | if genRequest.ImageEndUrl != "" && !strings.HasPrefix(genRequest.ImageEndUrl, "https://storage.cdn-luma.com/app_data/photon") { 117 | uploadRes, relayErr := uploadFile(genRequest.ImageEndUrl) 118 | if relayErr != nil { 119 | ReturnLumaError(c, relayErr.ErrorResp, relayErr.StatusCode) 120 | return 121 | } 122 | common.Logger.Infow("upload file success", "uploadRes", uploadRes) 123 | genRequest.ImageEndUrl = uploadRes.PublicUrl 124 | } 125 | 126 | reqData, _ := json.Marshal(genRequest) 127 | url := common.BaseUrl + SubmitEndpoint 128 | if strings.HasSuffix(c.Request.URL.Path, "/extend") { 129 | paths := strings.Split(c.Request.URL.Path, "/generations/") 130 | url += paths[1] 131 | } 132 | resp, err := DoRequest("POST", url, bytes.NewReader(reqData), nil) 133 | if err != nil { 134 | WrapperLumaError(c, err, http.StatusInternalServerError) 135 | return 136 | } 137 | defer resp.Body.Close() 138 | 139 | if resp.StatusCode >= 400 { 140 | c.Writer.WriteHeader(resp.StatusCode) 141 | for key, values := range resp.Header { 142 | for _, value := range values { 143 | c.Writer.Header().Add(key, value) 144 | } 145 | } 146 | // 读取响应体 147 | _, err = io.Copy(c.Writer, resp.Body) 148 | if err != nil { 149 | WrapperLumaError(c, err, http.StatusInternalServerError) 150 | return 151 | } 152 | } 153 | 154 | body, err := io.ReadAll(resp.Body) 155 | if err != nil { 156 | WrapperLumaError(c, err, http.StatusInternalServerError) 157 | return 158 | } 159 | var res []any 160 | err = json.Unmarshal(body, &res) 161 | if err != nil { 162 | WrapperLumaError(c, err, http.StatusInternalServerError) 163 | return 164 | } 165 | 166 | c.JSON(resp.StatusCode, res[0]) 167 | } 168 | 169 | // support base64\url 170 | func uploadFile(imgFile string) (*FileUploadResult, *WrapperErrorResp) { 171 | signedUpload, relayErr := getSignedUpload() 172 | if relayErr != nil { 173 | return nil, relayErr 174 | } 175 | 176 | presignedURL := signedUpload.PresignedUrl 177 | 178 | file, err := readImage(imgFile) 179 | if err != nil { 180 | return nil, GenLumaError(err, http.StatusInternalServerError) 181 | } 182 | 183 | resp, err := DoRequest(http.MethodPut, presignedURL, file, map[string]string{ 184 | "Content-Type": "image/*", 185 | }) 186 | if err != nil { 187 | return nil, GenLumaError(err, http.StatusInternalServerError) 188 | } 189 | 190 | defer resp.Body.Close() 191 | 192 | if resp.StatusCode == http.StatusOK { 193 | return signedUpload, nil 194 | } 195 | body, err := io.ReadAll(resp.Body) 196 | if err != nil { 197 | return nil, GenLumaError(err, http.StatusInternalServerError) 198 | } 199 | return nil, GenLumaError(fmt.Errorf(string(body)), resp.StatusCode) 200 | } 201 | 202 | func getSignedUpload() (*FileUploadResult, *WrapperErrorResp) { 203 | url := common.BaseUrl + FileUploadEndpoint + "?file_type=image&filename=file.jpg" 204 | 205 | resp, err := DoRequest(http.MethodPost, url, nil, nil) 206 | if err != nil { 207 | return nil, GenLumaError(err, http.StatusInternalServerError) 208 | } 209 | 210 | defer resp.Body.Close() 211 | body, err := io.ReadAll(resp.Body) 212 | if err != nil { 213 | return nil, GenLumaError(err, http.StatusInternalServerError) 214 | } 215 | 216 | if resp.StatusCode >= 400 { 217 | return nil, GenLumaError(fmt.Errorf(string(body)), resp.StatusCode) 218 | } 219 | 220 | var result FileUploadResult 221 | err = json.Unmarshal(body, &result) 222 | if err != nil { 223 | return nil, GenLumaError(err, http.StatusInternalServerError) 224 | } 225 | return &result, nil 226 | } 227 | 228 | func readImage(image string) (io.Reader, error) { 229 | if strings.HasPrefix(image, "data:image/") { 230 | return getImageFromBase64(image) 231 | } 232 | return getImageFromUrl(image) 233 | } 234 | 235 | var ( 236 | reg = regexp.MustCompile(`data:image/([^;]+);base64,`) 237 | ) 238 | 239 | func getImageFromBase64(encoded string) (data io.Reader, err error) { 240 | decoded, err := base64.StdEncoding.DecodeString(reg.ReplaceAllString(encoded, "")) 241 | if err != nil { 242 | return nil, err 243 | } 244 | 245 | return bytes.NewReader(decoded), nil 246 | } 247 | 248 | func getImageFromUrl(url string) (data io.Reader, err error) { 249 | resp, err := http.Get(url) 250 | if err != nil { 251 | return 252 | } 253 | defer resp.Body.Close() 254 | dataBytes, err := io.ReadAll(resp.Body) 255 | if err != nil { 256 | return 257 | } 258 | return bytes.NewReader(dataBytes), nil 259 | } 260 | 261 | func getMe() (map[string]any, error) { 262 | resp, err := DoRequest(http.MethodGet, common.BaseUrl+UserinfoEndpoint, nil, nil) 263 | if err != nil { 264 | return nil, err 265 | } 266 | if resp.StatusCode != http.StatusOK { 267 | return nil, errors.New(resp.Status) 268 | } 269 | var res = make(map[string]any) 270 | err = json.NewDecoder(resp.Body).Decode(&res) 271 | if err != nil { 272 | return nil, err 273 | } 274 | return res, nil 275 | } 276 | 277 | func getUsage() (map[string]any, error) { 278 | resp, err := DoRequest(http.MethodGet, common.BaseUrl+UsageEndpoint, nil, nil) 279 | if err != nil { 280 | return nil, err 281 | } 282 | if resp.StatusCode != http.StatusOK { 283 | return nil, errors.New(resp.Status) 284 | } 285 | var res = make(map[string]any) 286 | err = json.NewDecoder(resp.Body).Decode(&res) 287 | if err != nil { 288 | return nil, err 289 | } 290 | return res, nil 291 | } 292 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "log" 7 | "luma-api/common" 8 | "luma-api/middleware" 9 | "net/http" 10 | "runtime/debug" 11 | ) 12 | 13 | func main() { 14 | common.SetupLogger() 15 | common.Logger.Info("Luma-API started") 16 | 17 | if common.DebugEnabled { 18 | common.Logger.Info("running in debug mode") 19 | gin.SetMode(gin.ReleaseMode) 20 | } 21 | if common.PProfEnabled { 22 | common.SafeGoroutine(func() { 23 | log.Println(http.ListenAndServe("0.0.0.0:8005", nil)) 24 | }) 25 | common.Logger.Info("running in pprof") 26 | } 27 | common.InitTemplate() 28 | 29 | common.SafeGoroutine(func() { 30 | StartAllKeepAlive() 31 | }) 32 | 33 | // Initialize HTTP server 34 | server := gin.New() 35 | server.Use(middleware.RequestId()) 36 | server.Use(gin.CustomRecovery(func(c *gin.Context, err any) { 37 | common.Logger.Error(fmt.Sprintf("panic detected: %v, stack: %s", err, string(debug.Stack()))) 38 | c.JSON(http.StatusInternalServerError, gin.H{ 39 | "error": gin.H{ 40 | "message": fmt.Sprintf("Panic detected, error: %v. Please contact site admin", err), 41 | "type": "api_panic", 42 | }, 43 | }) 44 | })) 45 | server.Use(middleware.GinzapWithConfig()) 46 | 47 | RegisterRouter(server) 48 | 49 | common.Logger.Info("Start: 0.0.0.0:" + common.Port) 50 | err := server.Run(":" + common.Port) 51 | if err != nil { 52 | common.Logger.Fatal("failed to start HTTP server: " + err.Error()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "luma-api/common" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | func SecretAuth() func(c *gin.Context) { 11 | return func(c *gin.Context) { 12 | if common.SecretToken == "" { 13 | return 14 | } 15 | accessToken := c.Request.Header.Get("Authorization") 16 | accessToken = strings.TrimLeft(accessToken, "Bearer ") 17 | if accessToken == common.SecretToken { 18 | c.Next() 19 | } else { 20 | c.JSON(http.StatusUnauthorized, gin.H{ 21 | "detail": map[string]any{ 22 | "reason": "unauthorized secret token", 23 | "code": 1, 24 | }, 25 | }) 26 | c.Abort() 27 | return 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /middleware/logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | ginzap "github.com/gin-contrib/zap" 5 | "github.com/gin-gonic/gin" 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | "luma-api/common" 9 | "time" 10 | ) 11 | 12 | func GinzapWithConfig() func(c *gin.Context) { 13 | return func(c *gin.Context) { 14 | ginzap.GinzapWithConfig(common.LoggerZap, &ginzap.Config{ 15 | UTC: true, 16 | TimeFormat: time.RFC3339, 17 | Context: ginzap.Fn(func(c *gin.Context) []zapcore.Field { 18 | fields := []zapcore.Field{} 19 | // log request ID 20 | if requestID := c.Writer.Header().Get(RequestIdKey); requestID != "" { 21 | fields = append(fields, zap.String("request_id", requestID)) 22 | } 23 | 24 | // log trace and span ID 25 | /*if trace.SpanFromContext(c.Request.Context()).SpanContext().IsValid() { 26 | fields = append(fields, zap.String("trace_id", trace.SpanFromContext(c.Request.Context()).SpanContext().TraceID().String())) 27 | fields = append(fields, zap.String("span_id", trace.SpanFromContext(c.Request.Context()).SpanContext().SpanID().String())) 28 | }*/ 29 | 30 | // log request body 31 | /*var body []byte 32 | var buf bytes.Buffer 33 | tee := io.TeeReader(c.Request.Body, &buf) 34 | body, _ = io.ReadAll(tee) 35 | c.Request.Body = io.NopCloser(&buf) 36 | fields = append(fields, zap.String("body", string(body)))*/ 37 | 38 | return fields 39 | }), 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /middleware/request-id.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "github.com/gin-gonic/gin" 6 | "luma-api/common" 7 | ) 8 | 9 | const RequestIdKey = "X-Any2api-Request-Id" 10 | 11 | func RequestId() func(c *gin.Context) { 12 | return func(c *gin.Context) { 13 | id := common.GetTimeString() + common.GetRandomString(8) 14 | c.Set(RequestIdKey, id) 15 | ctx := context.WithValue(c.Request.Context(), RequestIdKey, id) 16 | c.Request = c.Request.WithContext(ctx) 17 | c.Header(RequestIdKey, id) 18 | c.Next() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /models.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type GenRequest struct { 4 | UserPrompt string `json:"user_prompt"` // option 5 | AspectRatio string `json:"aspect_ratio,omitempty"` // option 6 | ExpandPrompt bool `json:"expand_prompt,omitempty"` // option 7 | ImageUrl string `json:"image_url,omitempty"` //option, uploaded refer image url 8 | ImageEndUrl string `json:"image_end_url,omitempty"` //option, uploaded refer image url 9 | } 10 | 11 | type VideoTask struct { 12 | ID string `json:"id"` 13 | Prompt string `json:"prompt"` 14 | State string `json:"state"` // "pending", "processing", "completed" 15 | CreatedAt string `json:"created_at"` 16 | Video *Video `json:"video"` 17 | Liked interface{} `json:"liked"` 18 | EstimateWaitSeconds interface{} `json:"estimate_wait_seconds"` 19 | } 20 | 21 | type Video struct { 22 | Url string `json:"url"` 23 | Width int `json:"width"` 24 | Height int `json:"height"` 25 | Thumbnail interface{} `json:"thumbnail"` 26 | } 27 | 28 | type UploadReq struct { 29 | Url string `json:"url"` // support public url & base64 30 | } 31 | 32 | type FileUploadResult struct { 33 | Id string `json:"id"` 34 | PresignedUrl string `json:"presigned_url"` 35 | PublicUrl string `json:"public_url"` 36 | } 37 | 38 | type WrapperErrorResp struct { 39 | ErrorResp ErrorResp 40 | StatusCode int 41 | } 42 | 43 | type ErrorResp struct { 44 | Detail string `json:"detail"` 45 | } 46 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | tls_client "github.com/bogdanfinn/tls-client" 5 | "github.com/bogdanfinn/tls-client/profiles" 6 | "io" 7 | "luma-api/common" 8 | "net/http" 9 | "net/url" 10 | ) 11 | 12 | var TlsHTTPClient tls_client.HttpClient 13 | var HTTPClient *http.Client 14 | 15 | func init() { 16 | InitTlsHTTPClient() 17 | } 18 | 19 | func InitTlsHTTPClient() { 20 | jar := tls_client.NewCookieJar() 21 | options := []tls_client.HttpClientOption{ 22 | tls_client.WithTimeoutSeconds(360), 23 | tls_client.WithClientProfile(profiles.Chrome_120), 24 | tls_client.WithNotFollowRedirects(), 25 | tls_client.WithCookieJar(jar), // create cookieJar instance and pass it as argument 26 | } 27 | client, _ := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...) 28 | TlsHTTPClient = client 29 | 30 | HTTPClient = &http.Client{} 31 | if common.Proxy != "" { 32 | TlsHTTPClient.SetProxy(common.Proxy) 33 | 34 | u, err := url.Parse(common.Proxy) 35 | if err != nil { 36 | common.Logger.Error("Invalid proxy URL: " + common.Proxy) 37 | panic(err) 38 | } 39 | HTTPClient = &http.Client{ 40 | Transport: &http.Transport{ 41 | Proxy: http.ProxyURL(u), 42 | }, 43 | } 44 | } 45 | } 46 | 47 | func DoRequest(method, url string, body io.Reader, otherHeaders map[string]string) (*http.Response, error) { 48 | headers := map[string]string{ 49 | "Cookie": GetLumaAuth(), 50 | } 51 | for k, v := range CommonHeaders { 52 | headers[k] = v 53 | } 54 | for k, v := range otherHeaders { 55 | headers[k] = v 56 | } 57 | 58 | req, err := http.NewRequest(method, url, body) 59 | if err != nil { 60 | return nil, err 61 | } 62 | defer func() { 63 | if body != nil { 64 | req.Body.Close() 65 | } 66 | }() 67 | 68 | for k, v := range headers { 69 | req.Header.Set(k, v) 70 | } 71 | 72 | resp, err := HTTPClient.Do(req) 73 | if err != nil { 74 | return nil, err 75 | } 76 | HandleRespCookies(resp) 77 | return resp, nil 78 | } 79 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | swaggerfiles "github.com/swaggo/files" 6 | ginSwagger "github.com/swaggo/gin-swagger" 7 | "luma-api/docs" 8 | "luma-api/middleware" 9 | ) 10 | 11 | func RegisterRouter(r *gin.Engine) { 12 | r.GET("/ping", func(c *gin.Context) { 13 | c.JSON(200, gin.H{ 14 | "message": "pong", 15 | }) 16 | }) 17 | docs.SwaggerInfo.BasePath = "/" 18 | r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) 19 | 20 | apiRouter := r.Group("/luma", middleware.SecretAuth()) 21 | { 22 | //old 废弃 23 | apiRouter.POST("/generations/", Generations) 24 | 25 | // current 26 | //apiRouter.GET("/generations/*action", Fetch) 27 | 28 | // submit 29 | apiRouter.POST("/generations", Generations) 30 | apiRouter.POST("/generations/:task_id/extend", ExtentGenerations) 31 | apiRouter.POST("/generations/file_upload", Upload) 32 | 33 | // get data 34 | apiRouter.GET("/generations/:task_id", FetchByID) 35 | apiRouter.GET("/generations/:task_id/download_video_url", GetDownloadUrl) 36 | 37 | apiRouter.GET("/subscription/usage", Usage) 38 | apiRouter.GET("/users/me", Me) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /template/luma.yaml: -------------------------------------------------------------------------------- 1 | chat_stream_submit: | 2 | 请求成功,正在生成视频,请稍等片刻... 3 | 4 | chat_stream_tick: ~ 5 | 6 | chat_resp: | 7 | **视频大小: ** {{.video.width}}x{{.video.height}} 8 | **资源链接:** 9 | - 🎬 视频: [点击观看]({{.video.url}}) 10 | 11 | 12 | --------------------------------------------------------------------------------