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