图生视频

This commit is contained in:
renzhiyuan 2025-05-19 11:48:56 +08:00
commit 69bdad7090
8 changed files with 614 additions and 0 deletions

40
ans.go Normal file
View File

@ -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)
}

87
crypt.go Normal file
View File

@ -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
}

BIN
fox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

78
func.go Normal file
View File

@ -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] == ':')
}

18
go.mod Normal file
View File

@ -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
)

239
main.go Normal file
View File

@ -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)
}

8
test.go Normal file
View File

@ -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"
}

144
video.go Normal file
View File

@ -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()
}