├── .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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------