working version
This commit is contained in:
parent
0b843aff0b
commit
db5cc78797
|
@ -0,0 +1,3 @@
|
|||
.aider*
|
||||
.env
|
||||
*.png
|
|
@ -0,0 +1,5 @@
|
|||
module gonamer
|
||||
|
||||
go 1.23.4
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
|
@ -0,0 +1,4 @@
|
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -0,0 +1,375 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ImageExtensionRegex is a regular expression to validate image file extensions.
|
||||
var ImageExtensionRegex = regexp.MustCompile(`(?i)\.(jpg|jpeg|png|gif)$`)
|
||||
|
||||
// OpenAIRequest represents the structure of the request to the OpenAI API.
|
||||
type OpenAIRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []Message `json:"messages"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
}
|
||||
|
||||
// Message represents a message in the OpenAI API request.
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content []Content `json:"content"`
|
||||
}
|
||||
|
||||
type Content struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ImageURL *ImageURL `json:"image_url,omitempty"`
|
||||
}
|
||||
|
||||
type ImageURL struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// OpenAIResponse represents the structure of the response from the OpenAI API.
|
||||
type OpenAIResponse struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
// Config represents the structure of the configuration file.
|
||||
type Config struct {
|
||||
APIKey string `yaml:"apikey"`
|
||||
Model string `yaml:"model"`
|
||||
Endpoint string `yaml:"endpoint"`
|
||||
Temperature float64 `yaml:"temperature"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Get the config file path
|
||||
configFilePath, err := getConfigFilePath()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting config file path: %v", err)
|
||||
}
|
||||
|
||||
// Load the configuration from the file
|
||||
config, err := loadConfig(configFilePath)
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading config: %v", err)
|
||||
}
|
||||
|
||||
// Get the image filename from the command-line arguments
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage: go run main.go <image_filename>")
|
||||
return
|
||||
}
|
||||
imageFilename := os.Args[1]
|
||||
|
||||
// Validate the image filename extension
|
||||
if !ImageExtensionRegex.MatchString(imageFilename) {
|
||||
fmt.Println("Error: Invalid image file extension. Only .jpg, .jpeg, .png, and .gif are supported.")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the suggested filename from the LLM
|
||||
newFilename, err := getSuggestedFilename(imageFilename, config)
|
||||
if err != nil {
|
||||
fmt.Println("Error getting suggested filename:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Rename the file
|
||||
err = renameFile(imageFilename, newFilename)
|
||||
if err != nil {
|
||||
fmt.Println("Error renaming file:", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully renamed '%s' to '%s'\n", imageFilename, newFilename)
|
||||
}
|
||||
|
||||
// getConfigFilePath returns the path to the config file.
|
||||
func getConfigFilePath() (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(homeDir, ".config", "gonamer.yaml"), nil
|
||||
}
|
||||
|
||||
// loadConfig loads the configuration from the specified file.
|
||||
func loadConfig(configFilePath string) (*Config, error) {
|
||||
data, err := ioutil.ReadFile(configFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config Config
|
||||
err = yaml.Unmarshal(data, &config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// getSuggestedFilename sends the image to the LLM and returns the suggested filename.
|
||||
func getSuggestedFilename(imageFilename string, config *Config) (string, error) {
|
||||
// 1. Encode the image to base64
|
||||
imageData, err := os.ReadFile(imageFilename)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading image file: %w", err)
|
||||
}
|
||||
base64Image := base64.StdEncoding.EncodeToString(imageData)
|
||||
|
||||
// 2. Create the request payload
|
||||
requestData := OpenAIRequest{
|
||||
Model: config.Model,
|
||||
Messages: []Message{
|
||||
{
|
||||
Role: "user",
|
||||
Content: []Content{
|
||||
{
|
||||
Type: "text",
|
||||
Text: "Suggest a useful, descriptive, and unique filename for this image, considering its content and context. Only provide the filename, no other text or explanation. Do not include the file extension.",
|
||||
},
|
||||
{
|
||||
Type: "image_url",
|
||||
ImageURL: &ImageURL{
|
||||
URL: fmt.Sprintf("data:image/%s;base64,%s", strings.TrimPrefix(filepath.Ext(imageFilename), "."), base64Image),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Temperature: config.Temperature,
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(requestData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error marshalling request data: %w", err)
|
||||
}
|
||||
|
||||
// 3. Send the request to the LLM (using JSON request as default)
|
||||
originalExtension := filepath.Ext(imageFilename)
|
||||
newFilename, err := sendJSONRequest(requestBody, originalExtension, config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return newFilename, nil
|
||||
}
|
||||
|
||||
// sendJSONRequest sends the request as JSON to the LLM endpoint
|
||||
func sendJSONRequest(requestBody []byte, originalExtension string, config *Config) (string, error) {
|
||||
req, err := http.NewRequest("POST", config.Endpoint, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating HTTP request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+config.APIKey)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error sending HTTP request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 4. Process the response
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading response body: %w", err)
|
||||
}
|
||||
|
||||
// Print the raw response body
|
||||
//fmt.Println("Raw Response Body:")
|
||||
//fmt.Println(string(responseBody))
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("API error (status %d): %s", resp.StatusCode, responseBody)
|
||||
}
|
||||
|
||||
var apiResponse OpenAIResponse
|
||||
err = json.Unmarshal(responseBody, &apiResponse)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error unmarshalling response: %w", err)
|
||||
}
|
||||
|
||||
if len(apiResponse.Choices) == 0 || apiResponse.Choices[0].Message.Content == "" {
|
||||
return "", fmt.Errorf("empty response from API")
|
||||
}
|
||||
|
||||
suggestedFilename := strings.TrimSpace(apiResponse.Choices[0].Message.Content)
|
||||
|
||||
// Remove any existing extension from the suggested filename
|
||||
suggestedFilename = strings.TrimSuffix(suggestedFilename, filepath.Ext(suggestedFilename))
|
||||
|
||||
// Ensure the suggested filename has the correct extension
|
||||
if !strings.HasSuffix(strings.ToLower(suggestedFilename), strings.ToLower(originalExtension)) {
|
||||
suggestedFilename += originalExtension
|
||||
}
|
||||
|
||||
return suggestedFilename, nil
|
||||
}
|
||||
|
||||
// sendMultipartRequest sends the request as multipart/form-data to the LLM endpoint (alternative)
|
||||
func sendMultipartRequest(requestBody []byte, originalExtension string, config *Config) (string, error) {
|
||||
// Prepare a form
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
// Add the JSON part
|
||||
jsonPart, err := writer.CreateFormField("json")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating form field: %w", err)
|
||||
}
|
||||
_, err = jsonPart.Write(requestBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error writing to form field: %w", err)
|
||||
}
|
||||
|
||||
// Close the multipart writer
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error closing multipart writer: %w", err)
|
||||
}
|
||||
|
||||
// Create a new request
|
||||
req, err := http.NewRequest("POST", config.Endpoint, body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating HTTP request: %w", err)
|
||||
}
|
||||
|
||||
// Set headers
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("Authorization", "Bearer "+config.APIKey)
|
||||
|
||||
// Send the request
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error sending HTTP request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Process the response (same as in sendJSONRequest)
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading response body: %w", err)
|
||||
}
|
||||
|
||||
// Print the raw response body
|
||||
//fmt.Println("Raw Response Body:")
|
||||
//fmt.Println(string(responseBody))
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("API error (status %d): %s", resp.StatusCode, responseBody)
|
||||
}
|
||||
|
||||
var apiResponse OpenAIResponse
|
||||
err = json.Unmarshal(responseBody, &apiResponse)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error unmarshalling response: %w", err)
|
||||
}
|
||||
|
||||
if len(apiResponse.Choices) == 0 || apiResponse.Choices[0].Message.Content == "" {
|
||||
return "", fmt.Errorf("empty response from API")
|
||||
}
|
||||
|
||||
suggestedFilename := strings.TrimSpace(apiResponse.Choices[0].Message.Content)
|
||||
|
||||
// Remove any existing extension from the suggested filename
|
||||
suggestedFilename = strings.TrimSuffix(suggestedFilename, filepath.Ext(suggestedFilename))
|
||||
|
||||
// Ensure the suggested filename has the correct extension
|
||||
if !strings.HasSuffix(strings.ToLower(suggestedFilename), strings.ToLower(originalExtension)) {
|
||||
suggestedFilename += originalExtension
|
||||
}
|
||||
|
||||
return suggestedFilename, nil
|
||||
}
|
||||
|
||||
// renameFile renames the file, handling potential issues like existing files.
|
||||
func renameFile(oldFilename, newFilename string) error {
|
||||
// Sanitize the new filename (remove invalid characters, etc.)
|
||||
newFilename = sanitizeFilename(newFilename)
|
||||
|
||||
// Check if a file with the new name already exists
|
||||
if _, err := os.Stat(newFilename); err == nil {
|
||||
// If it exists, generate a unique name (e.g., by adding a number)
|
||||
newFilename = makeUniqueFilename(newFilename)
|
||||
}
|
||||
|
||||
// Perform the rename operation
|
||||
return os.Rename(oldFilename, newFilename)
|
||||
}
|
||||
|
||||
// sanitizeFilename removes invalid characters from a filename and ensures the first character is a letter.
|
||||
func sanitizeFilename(filename string) string {
|
||||
// Extract the extension
|
||||
ext := filepath.Ext(filename)
|
||||
filenameWithoutExt := filename[:len(filename)-len(ext)]
|
||||
|
||||
// Define a regular expression to match invalid filename characters
|
||||
invalidChars := regexp.MustCompile(`[\\/:*?"<>|]`)
|
||||
|
||||
// Replace invalid characters with underscores
|
||||
sanitizedFilename := invalidChars.ReplaceAllString(filenameWithoutExt, "_")
|
||||
|
||||
// Remove non-letter characters from the beginning of the filename
|
||||
for len(sanitizedFilename) > 0 && !unicode.IsLetter(rune(sanitizedFilename[0])) {
|
||||
sanitizedFilename = sanitizedFilename[1:]
|
||||
}
|
||||
|
||||
// Ensure the first character is a letter
|
||||
if len(sanitizedFilename) == 0 || !unicode.IsLetter(rune(sanitizedFilename[0])) {
|
||||
sanitizedFilename = "A_" + sanitizedFilename
|
||||
}
|
||||
|
||||
// Remove leading dots and underscores
|
||||
for len(sanitizedFilename) > 0 && (sanitizedFilename[0] == '.' || sanitizedFilename[0] == '_') {
|
||||
sanitizedFilename = sanitizedFilename[1:]
|
||||
}
|
||||
|
||||
// If the filename is now empty (e.g., it was only dots or underscores), set it to "A"
|
||||
if len(sanitizedFilename) == 0 {
|
||||
sanitizedFilename = "A"
|
||||
}
|
||||
|
||||
return sanitizedFilename + ext
|
||||
}
|
||||
|
||||
// makeUniqueFilename adds a number to the filename to make it unique.
|
||||
func makeUniqueFilename(filename string) string {
|
||||
ext := filepath.Ext(filename)
|
||||
name := filename[:len(filename)-len(ext)]
|
||||
|
||||
counter := 1
|
||||
newFilename := filename
|
||||
for {
|
||||
if _, err := os.Stat(newFilename); os.IsNotExist(err) {
|
||||
return newFilename
|
||||
}
|
||||
newFilename = fmt.Sprintf("%s_%d%s", name, counter, ext)
|
||||
counter++
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue