rewrite into go and nextjs
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
junk2jive-server
|
||||||
|
.env
|
||||||
|
.direnv
|
||||||
15
Makefile
Normal file
15
Makefile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
env ?= development
|
||||||
|
port ?= 8080
|
||||||
|
|
||||||
|
junk2jive:
|
||||||
|
@go build ./cmd/junk2jive
|
||||||
|
run:
|
||||||
|
@go run ./cmd/junk2jive -e $(env) -p $(port)
|
||||||
|
test:
|
||||||
|
@go test ./... -v
|
||||||
|
test_coverage:
|
||||||
|
# @go test -cover ./...
|
||||||
|
@go test -coverprofile=coverage.out ./...
|
||||||
|
@go tool cover -func=coverage.out | grep total: | awk '{print "Total coverage: " $$3}'
|
||||||
|
clean:
|
||||||
|
@rm ./junk2jive
|
||||||
58
cmd/junk2jive/main.go
Normal file
58
cmd/junk2jive/main.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config"
|
||||||
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers"
|
||||||
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/routes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Parse command line flags
|
||||||
|
var configPath string
|
||||||
|
flag.StringVar(&configPath, "config", "", "Path to config file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// If no config file specified, look for it in default locations
|
||||||
|
if configPath == "" {
|
||||||
|
// Check current directory
|
||||||
|
if _, err := os.Stat("config.json"); err == nil {
|
||||||
|
configPath = "config.json"
|
||||||
|
} else {
|
||||||
|
// Check config directory relative to executable
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err == nil {
|
||||||
|
potentialPath := filepath.Join(filepath.Dir(exePath), "../config/config.json")
|
||||||
|
if _, err := os.Stat(potentialPath); err == nil {
|
||||||
|
configPath = potentialPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
cfg, err := config.Load(configPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load configuration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize services
|
||||||
|
// This would initialize your OpenAI and Robowflow services
|
||||||
|
// based on your configuration
|
||||||
|
|
||||||
|
// Set up the router
|
||||||
|
router := routes.SetupRoutes(cfg)
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||||
|
log.Printf("Starting server on %s", addr)
|
||||||
|
if err := http.ListenAndServe(addr, router); err != nil {
|
||||||
|
log.Fatalf("Failed to start server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
25
flake.lock
generated
Normal file
25
flake.lock
generated
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1746232882,
|
||||||
|
"narHash": "sha256-MHmBH2rS8KkRRdoU/feC/dKbdlMkcNkB5mwkuipVHeQ=",
|
||||||
|
"rev": "7a2622e2c0dbad5c4493cb268aba12896e28b008",
|
||||||
|
"revCount": 793418,
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.793418%2Brev-7a2622e2c0dbad5c4493cb268aba12896e28b008/0196974c-148c-7984-8656-db70973db21b/source.tar.gz"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://flakehub.com/f/NixOS/nixpkgs/0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
38
flake.nix
Normal file
38
flake.nix
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
description = "A Nix-flake-based Go 1.22 development environment";
|
||||||
|
|
||||||
|
inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1";
|
||||||
|
|
||||||
|
outputs = inputs:
|
||||||
|
let
|
||||||
|
goVersion = 23; # Change this to update the whole stack
|
||||||
|
|
||||||
|
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
||||||
|
forEachSupportedSystem = f: inputs.nixpkgs.lib.genAttrs supportedSystems (system: f {
|
||||||
|
pkgs = import inputs.nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [ inputs.self.overlays.default ];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
in
|
||||||
|
{
|
||||||
|
overlays.default = final: prev: {
|
||||||
|
go = final."go_1_${toString goVersion}";
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells = forEachSupportedSystem ({ pkgs }: {
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
# go (version is specified by overlay)
|
||||||
|
go
|
||||||
|
|
||||||
|
# goimports, godoc, etc.
|
||||||
|
gotools
|
||||||
|
|
||||||
|
# https://github.com/golangci/golangci-lint
|
||||||
|
golangci-lint
|
||||||
|
];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
3
go.mod
Normal file
3
go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module gitea.miguelmuniz.com/rogueking/junk2jive-server
|
||||||
|
|
||||||
|
go 1.24.2
|
||||||
26
internal/config/config.go
Normal file
26
internal/config/config.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
OpenAIKey string
|
||||||
|
RoboflowKey string
|
||||||
|
Port string
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
OpenAIKey: os.Getenv("OPENAI_API_KEY"),
|
||||||
|
RoboflowKey: os.Getenv("ROBOFLOW_API_KEY"),
|
||||||
|
Port: getEnvWithDefault("PORT", "8080"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvWithDefault(key, defaultValue string) string {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
0
internal/handlers/home.go
Normal file
0
internal/handlers/home.go
Normal file
43
internal/handlers/text.go
Normal file
43
internal/handlers/text.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.miguelmuniz.com/junk2jive-server/internal/config"
|
||||||
|
"gitea.miguelmuniz.com/junk2jive-server/internal/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TextPromptRequest struct {
|
||||||
|
Query string `json:"query"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DIYResponse struct {
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
Result string `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TextPromptHandler(cfg *config.Config) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req TextPromptRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
openAI := services.NewOpenAIService(cfg.OpenAIKey)
|
||||||
|
result, err := openAI.GenerateDIY(req.Query)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to generate DIY ideas", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := DIYResponse{
|
||||||
|
Prompt: "AI Suggestions for " + req.Query + " are:",
|
||||||
|
Result: result,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
72
internal/handlers/visual.go
Normal file
72
internal/handlers/visual.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.miguelmuniz.com/junk2jive-server/internal/config"
|
||||||
|
"gitea.miguelmuniz.com/junk2jive-server/internal/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func VisualAIHandler(cfg *config.Config) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse multipart form with 10MB limit
|
||||||
|
if err := r.ParseMultipartForm(10 << 20); err != nil {
|
||||||
|
http.Error(w, "Unable to parse form", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, handler, err := r.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error retrieving the file", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Create temporary file
|
||||||
|
tempFile, err := os.CreateTemp("", "upload-*"+filepath.Ext(handler.Filename))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error creating temporary file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.Remove(tempFile.Name())
|
||||||
|
defer tempFile.Close()
|
||||||
|
|
||||||
|
// Copy uploaded file to temp file
|
||||||
|
if _, err = io.Copy(tempFile, file); err != nil {
|
||||||
|
http.Error(w, "Error saving the file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process with Roboflow
|
||||||
|
roboflowService := services.NewRoboflowService(cfg.RoboflowKey)
|
||||||
|
detectedObjects, err := roboflowService.DetectObjects(tempFile.Name())
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error detecting objects", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate DIY ideas based on detected objects
|
||||||
|
openAI := services.NewOpenAIService(cfg.OpenAIKey)
|
||||||
|
query := strings.Join(detectedObjects, ", ")
|
||||||
|
result, err := openAI.GenerateDIY(query)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to generate DIY ideas", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare response
|
||||||
|
response := DIYResponse{
|
||||||
|
Prompt: fmt.Sprintf("AI Suggestions for %s are:", query),
|
||||||
|
Result: result,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
38
internal/routes/routes.go
Normal file
38
internal/routes/routes.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
"github.com/go-chi/cors"
|
||||||
|
"gitea.miguelmuniz.com/junk2jive-server/internal/config"
|
||||||
|
"gitea.miguelmuniz.com/junk2jive-server/internal/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupRoutes(cfg *config.Config) *chi.Mux {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
r.Use(middleware.Logger)
|
||||||
|
r.Use(middleware.Recoverer)
|
||||||
|
r.Use(cors.Handler(cors.Options{
|
||||||
|
AllowedOrigins: []string{"*"},
|
||||||
|
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
|
||||||
|
AllowedHeaders: []string{"Accept", "Content-Type"},
|
||||||
|
AllowCredentials: true,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
fileServer := http.FileServer(http.Dir("./static"))
|
||||||
|
r.Handle("/static/*", http.StripPrefix("/static", fileServer))
|
||||||
|
|
||||||
|
// API routes
|
||||||
|
r.Get("/", handlers.HomeHandler)
|
||||||
|
r.Route("/api", func(r chi.Router) {
|
||||||
|
r.Post("/text-prompt", handlers.TextPromptHandler(cfg))
|
||||||
|
r.Post("/ai-prompt", handlers.VisualAIHandler(cfg))
|
||||||
|
})
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
87
internal/services/openai.go
Normal file
87
internal/services/openai.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OpenAIService struct {
|
||||||
|
apiKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenAIRequest struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Messages []Message `json:"messages"`
|
||||||
|
Temperature float64 `json:"temperature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenAIResponse struct {
|
||||||
|
Choices []struct {
|
||||||
|
Message struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
} `json:"message"`
|
||||||
|
} `json:"choices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOpenAIService(apiKey string) *OpenAIService {
|
||||||
|
return &OpenAIService{apiKey: apiKey}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OpenAIService) GenerateDIY(query string) (string, error) {
|
||||||
|
url := "https://api.openai.com/v1/chat/completions"
|
||||||
|
|
||||||
|
prompt := fmt.Sprintf("Generate creative DIY project ideas to repurpose or recycle the following: %s. Provide detailed instructions, materials needed, and steps to create each project.", query)
|
||||||
|
|
||||||
|
requestBody := OpenAIRequest{
|
||||||
|
Model: "gpt-4", // or "gpt-3.5-turbo" depending on your needs/access
|
||||||
|
Messages: []Message{
|
||||||
|
{
|
||||||
|
Role: "system",
|
||||||
|
Content: "You are a creative DIY expert who helps people repurpose and recycle items into useful projects.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Role: "user",
|
||||||
|
Content: prompt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Temperature: 0.7,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(requestBody)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.apiKey))
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var response OpenAIResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(response.Choices) == 0 {
|
||||||
|
return "", fmt.Errorf("no response from OpenAI")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Choices[0].Message.Content, nil
|
||||||
|
}
|
||||||
85
internal/services/robowflow.go
Normal file
85
internal/services/robowflow.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RoboflowService struct {
|
||||||
|
apiKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoboflowResponse struct {
|
||||||
|
Predictions []struct {
|
||||||
|
Class string `json:"class"`
|
||||||
|
Confidence float64 `json:"confidence"`
|
||||||
|
} `json:"predictions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRoboflowService(apiKey string) *RoboflowService {
|
||||||
|
return &RoboflowService{apiKey: apiKey}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RoboflowService) DetectObjects(imagePath string) ([]string, error) {
|
||||||
|
// Open the file
|
||||||
|
file, err := os.Open(imagePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Create a new multipart writer
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
|
// Create a form file field
|
||||||
|
part, err := writer.CreateFormFile("file", filepath.Base(imagePath))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the file content to the form field
|
||||||
|
if _, err = io.Copy(part, file); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the writer to finalize the form
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
// Create the request
|
||||||
|
url := fmt.Sprintf("https://detect.roboflow.com/taco-puuof/1?api_key=%s&confidence=60&overlap=30", s.apiKey)
|
||||||
|
req, err := http.NewRequest("POST", url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
// Send the request
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
var response RoboflowResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract object classes
|
||||||
|
var objects []string
|
||||||
|
for _, prediction := range response.Predictions {
|
||||||
|
objects = append(objects, prediction.Class)
|
||||||
|
}
|
||||||
|
|
||||||
|
return objects, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user