图生视频
This commit is contained in:
commit
69bdad7090
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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] == ':')
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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)
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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()
|
||||
}
|
Loading…
Reference in New Issue