commit 69bdad70907d9bb2f040da47a7c053cedcbf1e01 Author: renzhiyuan <465386466@qq.com> Date: Mon May 19 11:48:56 2025 +0800 图生视频 diff --git a/ans.go b/ans.go new file mode 100644 index 0000000..23bc00b --- /dev/null +++ b/ans.go @@ -0,0 +1,40 @@ +package main + +import "fmt" + +type ANS string + +const ( + reset ANS = "\033[0m" + red ANS = "\033[31m" + green ANS = "\033[32m" + yellow ANS = "\033[33m" + blue ANS = "\033[34m" + purple ANS = "\033[35m" + cyan ANS = "\033[36m" + white ANS = "\033[37m" +) + +func success(content string, arg ...interface{}) { + fmt.Printf("%s%s%s\n", green, fmt.Sprintf(content, arg...), reset) +} + +func fatal(content string, arg ...interface{}) { + fmt.Printf("%s%s%s\n", red, fmt.Sprintf(content, arg...), reset) +} + +func warning(content string, arg ...interface{}) { + fmt.Printf("%s%s%s\n", yellow, fmt.Sprintf(content, arg...), reset) +} + +func log(content string, arg ...interface{}) { + fmt.Printf("%s\n", fmt.Sprintf(content, arg...)) +} + +func input(content string, arg ...interface{}) { + fmt.Printf("\n%s%s%s", cyan, fmt.Sprintf(content, arg...), reset) +} + +func tip(content string, arg ...interface{}) { + fmt.Printf("\n*%s%s%s", purple, fmt.Sprintf(content, arg...), reset) +} diff --git a/crypt.go b/crypt.go new file mode 100644 index 0000000..33c6170 --- /dev/null +++ b/crypt.go @@ -0,0 +1,87 @@ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + "io" +) + +func padToLength(s string, length int, padChar rune) string { + if len(s) >= length { + return s + } + // 计算需要填充的字符数 + padding := make([]rune, length-len(s)) + for i := range padding { + padding[i] = padChar + } + // 将原字符串和填充字符组合起来 + return s + string(padding) +} + +// 加密函数 +func encrypt(key []byte, text string) (string, error) { + plaintext := []byte(text) + + // 创建一个新的cipher.Block + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + // 创建一个新的GCM模式的加密器 + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + // 创建一个nonce(随机数) + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return "", err + } + + // 加密数据 + ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) + + // 返回Base64编码的加密字符串 + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// 解密函数 +func decrypt(key []byte, encryptedText string) (string, error) { + // 解码Base64字符串 + ciphertext, err := base64.StdEncoding.DecodeString(encryptedText) + if err != nil { + return "", err + } + + // 创建一个新的cipher.Block + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + // 创建一个新的GCM模式的解密器 + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + // 分离nonce和实际加密数据 + nonceSize := gcm.NonceSize() + if len(ciphertext) < nonceSize { + return "", errors.New("ciphertext too short") + } + + nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return "", err + } + + return string(plaintext), nil +} diff --git a/fox.png b/fox.png new file mode 100644 index 0000000..bd94240 Binary files /dev/null and b/fox.png differ diff --git a/func.go b/func.go new file mode 100644 index 0000000..a3cdeb0 --- /dev/null +++ b/func.go @@ -0,0 +1,78 @@ +package main + +import ( + "fmt" + "net/http" + "net/url" + "strings" +) + +func isImage(contentType string) bool { + imageTypes := []string{ + "image/jpeg", + "image/png", + } + + for _, imageType := range imageTypes { + if strings.Contains(contentType, imageType) { + return true + } + } + return false +} + +func isURL(s string) bool { + // 检查字符串是否为空 + if len(s) == 0 { + return false + } + + // 使用url.Parse解析URL + u, err := url.Parse(s) + if err != nil { + return false + } + + // 检查URL的Scheme是否为空(即是否有协议,如http, https等) + if u.Scheme == "" { + return false + } + + // 检查URL的Host是否为空(即是否有主机名) + if u.Host == "" { + return false + } + + // 如果需要,可以进一步验证Scheme是否为允许的协议(如http, https) + // 例如: + // allowedSchemes := map[string]bool{"http": true, "https": true} + // if !allowedSchemes[u.Scheme] { + // return false + // } + + return true +} + +// checkURL checks if the URL is accessible and if the content is an image. +func checkURL(url string) (bool, error) { + resp, err := http.Get(url) + if err != nil { + return false, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return false, fmt.Errorf("failed to fetch URL, status code: %d", resp.StatusCode) + } + + contentType := resp.Header.Get("Content-Type") + if contentType == "" { + return false, nil + } + + return isImage(contentType), nil +} + +func isLocalPath(s string) bool { + return strings.HasPrefix(s, "/") || (len(s) >= 2 && s[1] == ':') +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a5b3e2b --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module ex_pic_to_video + +go 1.23.7 + +require ( + github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be + github.com/manifoldco/promptui v0.9.0 + github.com/volcengine/volcengine-go-sdk v1.1.8 +) + +require ( + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/volcengine/volc-sdk-golang v1.0.23 // indirect + golang.org/x/sys v0.0.0-20200918174421-af09f7315aff // indirect + gopkg.in/yaml.v2 v2.2.8 // indirect +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..6e0d854 --- /dev/null +++ b/main.go @@ -0,0 +1,239 @@ +package main + +import ( + "bufio" + "context" + "github.com/manifoldco/promptui" + "time" + + "encoding/json" + "fmt" + "github.com/common-nighthawk/go-figure" + "github.com/volcengine/volcengine-go-sdk/service/arkruntime" + "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" + "github.com/volcengine/volcengine-go-sdk/volcengine" + + "os" + "strings" +) + +var ( + reader = bufio.NewReader(os.Stdin) +) + +var statusMap = map[string]string{ + "queued": "排队中", + "running": "任务运行中", + "cancelled": "取消任务", + "succeeded": "任务成功", + "failed": "任务失败", +} + +type Conf struct { + Key string `json:"key"` + Model string `json:"model"` +} + +type Video struct { + Url string `json:"url"` + Text string `json:"text"` +} + +func setVideoInfo() *Video { + var video = new(Video) + img(video) + desc(video) + return video +} + +var c *Conf + +func main() { + + log("正在读取配置。。。") + c = getConf() + success("读取配置成功!请务必保证网络通畅") + client := arkruntime.NewClientWithApiKey(c.Key) + video := setVideoInfo() + //finish(client, video) + do(client, c, video) +} + +func do(client *arkruntime.Client, c *Conf, video *Video) { + ctx := context.Background() + createReq := model.CreateContentGenerationTaskRequest{ + Model: c.Model, + Content: []*model.CreateContentGenerationContentItem{ + { + // 文本提示词与参数组合 + Type: model.ContentGenerationContentItemTypeText, + Text: volcengine.String(video.Text), + }, + { + // 图片URL + Type: model.ContentGenerationContentItemTypeImage, + ImageURL: &model.ImageURL{ + URL: video.Url, //请上传可以访问的图片URL + }, + }, + }, + } + + createResponse, err := client.CreateContentGenerationTask(ctx, createReq) + if err != nil { + warning("创建视频失败: %v", err) + return + } + success("视频正在制作中,请稍后,taskId: %s", createResponse.ID) + listenTask(ctx, client, createResponse.ID, video) +} + +func listenTask(ctx context.Context, client *arkruntime.Client, taskId string, video *Video) { + var ( + req = model.GetContentGenerationTaskRequest{taskId} + ) + for { + time.Sleep(time.Second * 1) + resp, err := client.GetContentGenerationTask(ctx, req) + if err != nil { + warning("获取视频信息失败: %v", err) + continue + } + log(statusMap[resp.Status]) + if resp.Status == "succeeded" { + tip("本次消耗token: %d\n", resp.Usage.TotalTokens) + success("请复制下面地址到浏览器进行下载:\n") + fmt.Println(resp.Content.VideoURL) + finish(client, video) + } + } +} + +func finish(client *arkruntime.Client, video *Video) { + prompt := promptui.Select{ + Label: "", + Items: []string{"退出", "重新生成", "修改设置重新生成"}, + } + _, r, err := prompt.Run() + if err != nil { + warning("Prompt failed %v\n", err) + return + } + switch r { + case "退出": + prompt1 := promptui.Select{ + Label: "是否退出", + Items: []string{"否", "是"}, + } + _, q, _err := prompt1.Run() + if _err != nil { + warning("Prompt failed %v\n", err) + return + } + if q == "是" { + tip("最后祝您身体健康,再见") + exit() + } else { + finish(client, video) + } + + case "重新生成": + do(client, c, video) + case "修改设置重新生成": + video = setVideoInfo() + do(client, c, video) + } +} + +func getConf() *Conf { + wd, err := os.Getwd() + fp := fmt.Sprintf("%s/%s", wd, "ex_pic_to_video_conf") + if err != nil { + fatal("获取当前路径失败") + exit() + } + file, err := os.Open(fp) + if err != nil { + if !os.IsNotExist(err) { + os.Remove(fp) + } + return cConf(fp) + } + defer file.Close() + content, err := os.ReadFile(fp) + if err != nil { + fatal("获取配置信息失败") + exit() + } + + var ( + pwd string + conf = new(Conf) + ) + for { + input("请输入访问密码: ") + pwd, _ = reader.ReadString('\n') + cjson, err := decrypt([]byte(padToLength(strings.TrimSpace(pwd), 16, ' ')), string(content)) + if err != nil { + warning("密码错误") + continue + } + err = json.Unmarshal([]byte(cjson), &conf) + if err != nil { + warning("配置读取失败") + } + break + } + return conf +} + +func cConf(fp string) *Conf { + conf := new(Conf) + log("进入初始化配置向导") + log("----------------------") + + input("请设置key: ") + k, _ := reader.ReadString('\n') + conf.Key = strings.TrimSpace(k) + if conf.Key == "lsxd2025" { + conf.Key = "" + conf.Model = "" + myFigure := figure.NewFigure("LSXD", "", true) + myFigure.Print() + success("欢迎进入调试模式!") + return conf + } + + input("请设置model: ") + m, _ := reader.ReadString('\n') + conf.Model = strings.TrimSpace(m) + + var ( + pwd string + ) + for { + input("请设置访问密码(密码长度请小于16位): ") + pwd, _ = reader.ReadString('\n') + if len(strings.TrimSpace(pwd)) > 16 { + warning("密码长度请小于16位") + continue + } + break + } + cjson, _ := json.Marshal(conf) + pad := padToLength(strings.TrimSpace(pwd), 16, ' ') + en, e := encrypt([]byte(pad), string(cjson)) + if e != nil { + fatal("设置失败,程序退出:%v", e) + exit() + } + if err := os.WriteFile(fp, []byte(en), 0666); err != nil { + fatal("写入失败,程序退出") + exit() + } + return conf +} + +func exit() { + os.Exit(1) +} diff --git a/test.go b/test.go new file mode 100644 index 0000000..e1711b6 --- /dev/null +++ b/test.go @@ -0,0 +1,8 @@ +package main + +func test() { + //key = "236ba4b6-9daa-4755-b22f-2fd274cd223a" + //modelEp = "doubao-seedance-1-0-lite-i2v-250428" + //desc = "女孩抱着狐狸,女孩睁开眼,温柔地看向镜头,狐狸友善地抱着,镜头缓缓拉出,女孩的头发被风吹动 --resolution 480p --dur 5 --camerafixed false --watermark false" + //img = "https://ark-project.tos-cn-beijing.volces.com/doc_image/i2v_foxrgirl.png" +} diff --git a/video.go b/video.go new file mode 100644 index 0000000..69df87d --- /dev/null +++ b/video.go @@ -0,0 +1,144 @@ +package main + +import ( + "encoding/base64" + "fmt" + "github.com/manifoldco/promptui" + "os" + "path/filepath" + "strings" +) + +func img(video *Video) { + tip("图片信息,可以是图片URL或图片在此电脑上的本地图片。") + tip("图片URL:请确保图片URL可被访问。eg:https://ark-project.tos-cn-beijing.volces.com/doc_image/i2v_foxrgirl.png") + tip("本地图片:请确保图片存在的jpg/jpge/png图片,路径名称请用英文,避免不必要的错误。eg:C:\\Users\\Administrator\\Desktop\\sucai\\fox.png") + for { + input("请设置图片信息: ") + m, _ := reader.ReadString('\n') + video.Url = strings.TrimSpace(m) + if isURL(video.Url) { //判断是否为URL + isImg, err := checkURL(video.Url) + if !isImg { + warning("URL内容不是图片,请检查URL是否正确!") + continue + } + if err != nil { + warning("URL不可访问,请检查网络或URL是否正确!") + continue + } + } + + if isLocalPath(video.Url) { + fileExt := strings.ToLower(filepath.Ext(video.Url)) + var mimeType string + switch fileExt { + case ".jpg", ".jpeg": + mimeType = "jpeg" + case ".png": + mimeType = "png" + default: + warning("不支持的图片格式: %s", fileExt) + } + content, err := os.ReadFile(video.Url) + if err != nil { + warning("无法读取文件: %v", err) + continue + } + // 将文件内容编码为Base64字符串 + dataURL := fmt.Sprintf("data:image/%s;base64,%s", mimeType, base64.StdEncoding.EncodeToString(content)) + video.Url = dataURL + } + break + } +} + +func desc(video *Video) { + var d strings.Builder + tip("描述信息,请输入一段描述文字,描述文字越多,生成视频越精彩。") + input("请设置描述信息: ") + text, _ := reader.ReadString('\n') + d.WriteString(fmt.Sprintf("%s ", strings.TrimSpace(text))) + d.WriteString("--watermark ") + d.WriteString("false ") + for { + prompt := promptui.Select{ + Label: "分辨率: ", + Items: []string{"480p", "720p"}, + } + _, r, err := prompt.Run() + if err != nil { + warning("Prompt failed %v\n", err) + continue + } + d.WriteString("--resolution ") + d.WriteString(r) + d.WriteString(" ") + break + } + + for { + prompt := promptui.Select{ + Label: "视频时长(秒): ", + Items: []string{"5", "10"}, + } + _, r, err := prompt.Run() + if err != nil { + warning("Prompt failed %v\n", err) + continue + } + d.WriteString("--dur ") + d.WriteString(r) + d.WriteString(" ") + break + } + tip("adaptive:根据所上传图片的比例,自动选择最合适的宽高比。") + for { + prompt := promptui.Select{ + Label: "视频的宽高比例: ", + Items: []string{"adaptive", "16:9", "9:16", "4:3", "3:4", "21:9", "9:21", "1:1"}, + } + _, r, err := prompt.Run() + if err != nil { + warning("Prompt failed %v\n", err) + continue + } + d.WriteString("--ratio ") + d.WriteString(r) + d.WriteString(" ") + break + } + + for { + prompt := promptui.Select{ + Label: "帧率: ", + Items: []string{"16", "24"}, + } + _, r, err := prompt.Run() + if err != nil { + warning("Prompt failed %v\n", err) + continue + } + d.WriteString("--framepersecond ") + d.WriteString(r) + d.WriteString(" ") + break + } + + for { + prompt := promptui.Select{ + Label: "是否固定摄像头: ", + Items: []string{"false", "true"}, + } + _, r, err := prompt.Run() + if err != nil { + warning("Prompt failed %v\n", err) + continue + } + d.WriteString("--camerafixed ") + d.WriteString(r) + d.WriteString(" ") + break + } + video.Text = d.String() +}