create a cli tool for turning images into videos

This commit is contained in:
2023-12-22 23:14:48 -07:00
commit 6e2ed13e38
9 changed files with 389 additions and 0 deletions

284
cmd/cli/cli.go Normal file
View 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
View 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
},
}
}