create a cli tool for turning images into videos
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
i2v.exe
|
||||||
|
run_i2v.exe
|
||||||
|
run_i2v_gjr.exe
|
||||||
|
output.mp4
|
||||||
|
resp.txt
|
||||||
|
test.png
|
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
9
.idea/i2v.iml
generated
Normal file
9
.idea/i2v.iml
generated
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/i2v.iml" filepath="$PROJECT_DIR$/.idea/i2v.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
284
cmd/cli/cli.go
Normal file
284
cmd/cli/cli.go
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
b64 "encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
commands := []*cli.Command{
|
||||||
|
//startCmd(),
|
||||||
|
getJobResultCmd(),
|
||||||
|
}
|
||||||
|
|
||||||
|
app := &cli.App{
|
||||||
|
Name: "i2v",
|
||||||
|
Usage: "A command line tool to convert images to videos, based on Stability.ai's Rest API.",
|
||||||
|
Version: "v1.0.0",
|
||||||
|
Description: "i2v (Image to Video) is a command line tool to convert images to videos, based on Stability.ai's Rest API.",
|
||||||
|
Commands: commands,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "output",
|
||||||
|
Value: "./output.mp4",
|
||||||
|
Usage: "path to output file (should be a mp4)",
|
||||||
|
Aliases: []string{
|
||||||
|
"o",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Authors: []*cli.Author{
|
||||||
|
{
|
||||||
|
Name: "Mason Payne",
|
||||||
|
Email: "mason@masonitestudios.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Copyright: "2023 Masonite Studios LLC",
|
||||||
|
UseShortOptionHandling: true,
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
// use the first argument as the file name
|
||||||
|
fileLocation := c.Args().Get(0)
|
||||||
|
// if no argument is provided, use stdin
|
||||||
|
if fileLocation == "" {
|
||||||
|
fmt.Println("No file provided, using stdin.")
|
||||||
|
// read from stdin
|
||||||
|
var err error
|
||||||
|
fileLocationBytes, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading from stdin | %w", err)
|
||||||
|
}
|
||||||
|
fileLocation = string(fileLocationBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: preprocess the image to make sure it is the right size
|
||||||
|
/*
|
||||||
|
Supported Dimensions:
|
||||||
|
1024x576
|
||||||
|
576x1024
|
||||||
|
768x768
|
||||||
|
*/
|
||||||
|
|
||||||
|
// make sure the image is either jpeg or png
|
||||||
|
fileMimeType := mime.TypeByExtension(path.Ext(fileLocation))
|
||||||
|
if fileMimeType != "image/jpeg" && fileMimeType != "image/png" {
|
||||||
|
return fmt.Errorf("unsupported file type | %v", fileMimeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := initiateGeneratingAnimation(fileLocation)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error making request | %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Video is being rendered, this may take a while.")
|
||||||
|
fmt.Printf("Job ID: %v\n", id)
|
||||||
|
|
||||||
|
// wait for the job to finish
|
||||||
|
err = job(id, c.String("output"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting job result | %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
//fmt.Println("This package will be used for interacting with a running StormV2 service via a terminal or command line.")
|
||||||
|
err := app.Run(os.Args)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(fmt.Errorf("error running app | %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initiateGeneratingAnimation(fileLocation string) (string, error) {
|
||||||
|
// get base filename from file location
|
||||||
|
filename := path.Base(fileLocation)
|
||||||
|
|
||||||
|
// Create a buffer to store the request body
|
||||||
|
bodyBuf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
// Create a multipart writer with the buffer
|
||||||
|
writer := multipart.NewWriter(bodyBuf)
|
||||||
|
|
||||||
|
// Add form fields
|
||||||
|
err := writer.WriteField("seed", "0")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error writing form field seed | %w", err)
|
||||||
|
}
|
||||||
|
err = writer.WriteField("cfg_scale", "2.5")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error writing form field cfg_scale | %w", err)
|
||||||
|
}
|
||||||
|
err = writer.WriteField("motion_bucket_id", "40")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error writing form field motion_bucket_id | %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a file
|
||||||
|
file, err := os.Open(fileLocation)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error opening file | %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Create a form file part
|
||||||
|
//filePart, err := writer.CreateFormFile("image", filename)
|
||||||
|
filePart, err := CreateImageFormFile(writer, filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error creating form file | %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the file content to the form file part
|
||||||
|
_, err = io.Copy(filePart, file)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error copying file content | %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the multipart writer
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
url := "https://api.stability.ai/v2alpha/generation/image-to-video"
|
||||||
|
|
||||||
|
// Create the HTTP request
|
||||||
|
req, err := http.NewRequest("POST", url, bodyBuf)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error creating request | %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the Content-Type header
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
req.Header.Add("authorization", "Bearer "+os.Getenv("STABILITY_API_KEY"))
|
||||||
|
|
||||||
|
// Make the HTTP request
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error making request | %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Print the response status and body
|
||||||
|
fmt.Println("Status:", resp.Status)
|
||||||
|
fmt.Println("Body:", resp.Body)
|
||||||
|
|
||||||
|
//bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
//if err != nil {
|
||||||
|
// return "", fmt.Errorf("error reading response body | %w", err)
|
||||||
|
//}
|
||||||
|
//bodyString := string(bodyBytes)
|
||||||
|
//fmt.Println(bodyString)
|
||||||
|
|
||||||
|
res := struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Errors []string `json:"errors"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
// decode response body
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&res)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error decoding response body | %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// print response body
|
||||||
|
if res.Name != "" {
|
||||||
|
return "", fmt.Errorf("error generating animation | %v: Errors: %v", res.Name, strings.Join(res.Errors, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateImageFormFile(w *multipart.Writer, filename string) (io.Writer, error) {
|
||||||
|
fileMimeType := mime.TypeByExtension(path.Ext(filename))
|
||||||
|
h := make(textproto.MIMEHeader)
|
||||||
|
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "image", filename))
|
||||||
|
h.Set("Content-Type", fileMimeType)
|
||||||
|
return w.CreatePart(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getJobResult(jobID string) (string, bool, error) {
|
||||||
|
url := fmt.Sprintf("https://api.stability.ai/v2alpha/generation/image-to-video/result/%s", jobID)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, fmt.Errorf("error creating request | %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("authorization", "Bearer "+os.Getenv("STABILITY_API_KEY"))
|
||||||
|
req.Header.Add("accept", "application/json")
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, fmt.Errorf("error making request | %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
res := struct {
|
||||||
|
Video string `json:"video"`
|
||||||
|
FinishReason string `json:"finishReason"`
|
||||||
|
Seed int64 `json:"seed"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Errors []string `json:"errors"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&res)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, fmt.Errorf("error unmarshalling response body | %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Name != "" {
|
||||||
|
return "", false, fmt.Errorf("error generating animation | %v: Errors: %v", res.Name, strings.Join(res.Errors, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Status == "in-progress" {
|
||||||
|
return "", false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Video, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func job(id, outputLocation string) error {
|
||||||
|
var video string
|
||||||
|
var finished bool
|
||||||
|
var err error
|
||||||
|
// poll the job result until it is finished
|
||||||
|
for {
|
||||||
|
video, finished, err = getJobResult(id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting job result | %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if finished {
|
||||||
|
fmt.Println("Video has completed rendering.")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode the video, it is in base64 and is expected to be a mp4
|
||||||
|
decodedVideo, err := b64.StdEncoding.DecodeString(video)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error decoding video | %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the video to the current directory
|
||||||
|
err = os.WriteFile(outputLocation, decodedVideo, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error writing video to file | %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Video has been saved to ", outputLocation)
|
||||||
|
return nil
|
||||||
|
}
|
49
cmd/cli/commands.go
Normal file
49
cmd/cli/commands.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
//func startCmd() *cli.Command {
|
||||||
|
// return &cli.Command{
|
||||||
|
// Name: "start",
|
||||||
|
// Aliases: []string{"s"},
|
||||||
|
// Usage: "Start the application service",
|
||||||
|
// Action: func(c *cli.Context) error {
|
||||||
|
//
|
||||||
|
// return nil
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
func getJobResultCmd() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "get-job-result",
|
||||||
|
Aliases: []string{"gjr"},
|
||||||
|
Usage: "Get the result of a job",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "job-id",
|
||||||
|
Aliases: []string{"id"},
|
||||||
|
Usage: "The id of the job to get the result of",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
// get the job id
|
||||||
|
jobID := c.String("job-id")
|
||||||
|
|
||||||
|
// get the output file
|
||||||
|
output := c.String("output")
|
||||||
|
|
||||||
|
// get the job result
|
||||||
|
err := job(jobID, output)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error running job result checker | %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
11
go.mod
Normal file
11
go.mod
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module i2v
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require github.com/urfave/cli/v2 v2.26.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
|
)
|
8
go.sum
Normal file
8
go.sum
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
|
||||||
|
github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
Reference in New Issue
Block a user