This commit is contained in:
2025-02-23 14:59:04 +01:00
22 changed files with 1288 additions and 1 deletions

5
.gitignore vendored
View File

@ -1,2 +1,5 @@
.env
db
screenmark
node_modules
dist
dist

View File

@ -0,0 +1,18 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
)
type ImageLinks struct {
ID uuid.UUID `sql:"primary_key"`
Link string
ImageID uuid.UUID
}

View File

@ -0,0 +1,17 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
)
type ImageTags struct {
TagID uuid.UUID
ImageID uuid.UUID
}

View File

@ -0,0 +1,18 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
)
type ImageText struct {
ID uuid.UUID `sql:"primary_key"`
ImageText string
ImageID uuid.UUID
}

View File

@ -0,0 +1,19 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
)
type UserImages struct {
ID uuid.UUID `sql:"primary_key"`
ImageName string
Image []byte
UserID uuid.UUID
}

View File

@ -0,0 +1,18 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
)
type UserTags struct {
ID uuid.UUID `sql:"primary_key"`
Tag string
UserID uuid.UUID
}

View File

@ -0,0 +1,16 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
)
type Users struct {
ID uuid.UUID `sql:"primary_key"`
}

View File

@ -0,0 +1,81 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var ImageLinks = newImageLinksTable("haystack", "image_links", "")
type imageLinksTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Link postgres.ColumnString
ImageID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageLinksTable struct {
imageLinksTable
EXCLUDED imageLinksTable
}
// AS creates new ImageLinksTable with assigned alias
func (a ImageLinksTable) AS(alias string) *ImageLinksTable {
return newImageLinksTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageLinksTable with assigned schema name
func (a ImageLinksTable) FromSchema(schemaName string) *ImageLinksTable {
return newImageLinksTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageLinksTable with assigned table prefix
func (a ImageLinksTable) WithPrefix(prefix string) *ImageLinksTable {
return newImageLinksTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageLinksTable with assigned table suffix
func (a ImageLinksTable) WithSuffix(suffix string) *ImageLinksTable {
return newImageLinksTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageLinksTable(schemaName, tableName, alias string) *ImageLinksTable {
return &ImageLinksTable{
imageLinksTable: newImageLinksTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageLinksTableImpl("", "excluded", ""),
}
}
func newImageLinksTableImpl(schemaName, tableName, alias string) imageLinksTable {
var (
IDColumn = postgres.StringColumn("id")
LinkColumn = postgres.StringColumn("link")
ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{IDColumn, LinkColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{LinkColumn, ImageIDColumn}
)
return imageLinksTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Link: LinkColumn,
ImageID: ImageIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -0,0 +1,78 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var ImageTags = newImageTagsTable("haystack", "image_tags", "")
type imageTagsTable struct {
postgres.Table
// Columns
TagID postgres.ColumnString
ImageID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageTagsTable struct {
imageTagsTable
EXCLUDED imageTagsTable
}
// AS creates new ImageTagsTable with assigned alias
func (a ImageTagsTable) AS(alias string) *ImageTagsTable {
return newImageTagsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageTagsTable with assigned schema name
func (a ImageTagsTable) FromSchema(schemaName string) *ImageTagsTable {
return newImageTagsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageTagsTable with assigned table prefix
func (a ImageTagsTable) WithPrefix(prefix string) *ImageTagsTable {
return newImageTagsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageTagsTable with assigned table suffix
func (a ImageTagsTable) WithSuffix(suffix string) *ImageTagsTable {
return newImageTagsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageTagsTable(schemaName, tableName, alias string) *ImageTagsTable {
return &ImageTagsTable{
imageTagsTable: newImageTagsTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageTagsTableImpl("", "excluded", ""),
}
}
func newImageTagsTableImpl(schemaName, tableName, alias string) imageTagsTable {
var (
TagIDColumn = postgres.StringColumn("tag_id")
ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{TagIDColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{TagIDColumn, ImageIDColumn}
)
return imageTagsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
TagID: TagIDColumn,
ImageID: ImageIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -0,0 +1,81 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var ImageText = newImageTextTable("haystack", "image_text", "")
type imageTextTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
ImageText postgres.ColumnString
ImageID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageTextTable struct {
imageTextTable
EXCLUDED imageTextTable
}
// AS creates new ImageTextTable with assigned alias
func (a ImageTextTable) AS(alias string) *ImageTextTable {
return newImageTextTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageTextTable with assigned schema name
func (a ImageTextTable) FromSchema(schemaName string) *ImageTextTable {
return newImageTextTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageTextTable with assigned table prefix
func (a ImageTextTable) WithPrefix(prefix string) *ImageTextTable {
return newImageTextTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageTextTable with assigned table suffix
func (a ImageTextTable) WithSuffix(suffix string) *ImageTextTable {
return newImageTextTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageTextTable(schemaName, tableName, alias string) *ImageTextTable {
return &ImageTextTable{
imageTextTable: newImageTextTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageTextTableImpl("", "excluded", ""),
}
}
func newImageTextTableImpl(schemaName, tableName, alias string) imageTextTable {
var (
IDColumn = postgres.StringColumn("id")
ImageTextColumn = postgres.StringColumn("image_text")
ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{IDColumn, ImageTextColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{ImageTextColumn, ImageIDColumn}
)
return imageTextTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
ImageText: ImageTextColumn,
ImageID: ImageIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -0,0 +1,19 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
// UseSchema sets a new schema name for all generated table SQL builder types. It is recommended to invoke
// this method only once at the beginning of the program.
func UseSchema(schema string) {
ImageLinks = ImageLinks.FromSchema(schema)
ImageTags = ImageTags.FromSchema(schema)
ImageText = ImageText.FromSchema(schema)
UserImages = UserImages.FromSchema(schema)
UserTags = UserTags.FromSchema(schema)
Users = Users.FromSchema(schema)
}

View File

@ -0,0 +1,84 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var UserImages = newUserImagesTable("haystack", "user_images", "")
type userImagesTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
ImageName postgres.ColumnString
Image postgres.ColumnString
UserID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type UserImagesTable struct {
userImagesTable
EXCLUDED userImagesTable
}
// AS creates new UserImagesTable with assigned alias
func (a UserImagesTable) AS(alias string) *UserImagesTable {
return newUserImagesTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UserImagesTable with assigned schema name
func (a UserImagesTable) FromSchema(schemaName string) *UserImagesTable {
return newUserImagesTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UserImagesTable with assigned table prefix
func (a UserImagesTable) WithPrefix(prefix string) *UserImagesTable {
return newUserImagesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UserImagesTable with assigned table suffix
func (a UserImagesTable) WithSuffix(suffix string) *UserImagesTable {
return newUserImagesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUserImagesTable(schemaName, tableName, alias string) *UserImagesTable {
return &UserImagesTable{
userImagesTable: newUserImagesTableImpl(schemaName, tableName, alias),
EXCLUDED: newUserImagesTableImpl("", "excluded", ""),
}
}
func newUserImagesTableImpl(schemaName, tableName, alias string) userImagesTable {
var (
IDColumn = postgres.StringColumn("id")
ImageNameColumn = postgres.StringColumn("image_name")
ImageColumn = postgres.StringColumn("image")
UserIDColumn = postgres.StringColumn("user_id")
allColumns = postgres.ColumnList{IDColumn, ImageNameColumn, ImageColumn, UserIDColumn}
mutableColumns = postgres.ColumnList{ImageNameColumn, ImageColumn, UserIDColumn}
)
return userImagesTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
ImageName: ImageNameColumn,
Image: ImageColumn,
UserID: UserIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -0,0 +1,81 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var UserTags = newUserTagsTable("haystack", "user_tags", "")
type userTagsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Tag postgres.ColumnString
UserID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type UserTagsTable struct {
userTagsTable
EXCLUDED userTagsTable
}
// AS creates new UserTagsTable with assigned alias
func (a UserTagsTable) AS(alias string) *UserTagsTable {
return newUserTagsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UserTagsTable with assigned schema name
func (a UserTagsTable) FromSchema(schemaName string) *UserTagsTable {
return newUserTagsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UserTagsTable with assigned table prefix
func (a UserTagsTable) WithPrefix(prefix string) *UserTagsTable {
return newUserTagsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UserTagsTable with assigned table suffix
func (a UserTagsTable) WithSuffix(suffix string) *UserTagsTable {
return newUserTagsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUserTagsTable(schemaName, tableName, alias string) *UserTagsTable {
return &UserTagsTable{
userTagsTable: newUserTagsTableImpl(schemaName, tableName, alias),
EXCLUDED: newUserTagsTableImpl("", "excluded", ""),
}
}
func newUserTagsTableImpl(schemaName, tableName, alias string) userTagsTable {
var (
IDColumn = postgres.StringColumn("id")
TagColumn = postgres.StringColumn("tag")
UserIDColumn = postgres.StringColumn("user_id")
allColumns = postgres.ColumnList{IDColumn, TagColumn, UserIDColumn}
mutableColumns = postgres.ColumnList{TagColumn, UserIDColumn}
)
return userTagsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Tag: TagColumn,
UserID: UserIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -0,0 +1,75 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Users = newUsersTable("haystack", "users", "")
type usersTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type UsersTable struct {
usersTable
EXCLUDED usersTable
}
// AS creates new UsersTable with assigned alias
func (a UsersTable) AS(alias string) *UsersTable {
return newUsersTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UsersTable with assigned schema name
func (a UsersTable) FromSchema(schemaName string) *UsersTable {
return newUsersTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UsersTable with assigned table prefix
func (a UsersTable) WithPrefix(prefix string) *UsersTable {
return newUsersTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UsersTable with assigned table suffix
func (a UsersTable) WithSuffix(suffix string) *UsersTable {
return newUsersTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUsersTable(schemaName, tableName, alias string) *UsersTable {
return &UsersTable{
usersTable: newUsersTableImpl(schemaName, tableName, alias),
EXCLUDED: newUsersTableImpl("", "excluded", ""),
}
}
func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
var (
IDColumn = postgres.StringColumn("id")
allColumns = postgres.ColumnList{IDColumn}
mutableColumns = postgres.ColumnList{}
)
return usersTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

14
backend/go.mod Normal file
View File

@ -0,0 +1,14 @@
module screenmark/screenmark
go 1.24.0
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-jet/jet/v2 v2.12.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

17
backend/go.sum Normal file
View File

@ -0,0 +1,17 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-jet/jet/v2 v2.12.0 h1:z2JfvBAZgsfxlQz6NXBYdZTXc7ep3jhbszTLtETv1JE=
github.com/go-jet/jet/v2 v2.12.0/go.mod h1:ufQVRQeI1mbcO5R8uCEVcVf3Foej9kReBdwDx7YMWUM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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=

109
backend/main.go Normal file
View File

@ -0,0 +1,109 @@
package main
import (
"fmt"
"io"
"log"
"net/http"
"screenmark/screenmark/models"
"time"
"github.com/joho/godotenv"
"github.com/lib/pq"
)
func main() {
err := godotenv.Load()
if err != nil {
panic(err)
}
err = models.InitDatabase()
if err != nil {
panic(err)
}
listener := pq.NewListener(models.CONNECTION, time.Second, time.Second, func(event pq.ListenerEventType, err error) {
if err != nil {
panic(err)
}
})
defer listener.Close()
go func() {
err := listener.Listen("new_image")
if err != nil {
panic(err)
}
select {
case parameters := <-listener.Notify:
log.Println("received notification, new image available: " + parameters.Extra)
go func() {
openAiClient, err := CreateOpenAiClient()
if err != nil {
panic(err)
}
image, err := models.GetImage(parameters.Extra)
if err != nil {
log.Println(err)
return
}
imageInfo, err := openAiClient.GetImageInfo(image.ImageName, image.Image)
if err != nil {
log.Println(err)
return
}
log.Printf("Info: %+v\n", imageInfo)
}()
}
}()
mux := http.NewServeMux()
mux.HandleFunc("OPTIONS /image/{name}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Credentials", "*")
w.Header().Add("Access-Control-Allow-Headers", "*")
})
mux.HandleFunc("POST /image/{name}", func(w http.ResponseWriter, r *http.Request) {
imageName := r.PathValue("name")
userId := r.Header.Get("userId")
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Credentials", "*")
w.Header().Add("Access-Control-Allow-Headers", "*")
if len(imageName) == 0 {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "You need to provide a name in the path")
return
}
image, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Couldnt read the image from the request body")
return
}
err = models.SaveImage(userId, imageName, image)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Could not save image to DB")
return
}
})
log.Println("Listening and serving.")
http.ListenAndServe(":3040", mux)
}

View File

@ -0,0 +1,19 @@
package models
import (
"database/sql"
_ "github.com/lib/pq"
)
const CONNECTION = "postgresql://localhost:5432/haystack?sslmode=disable"
var db *sql.DB
func InitDatabase() error {
database, err := sql.Open("postgres", CONNECTION)
db = database
return err
}

33
backend/models/image.go Normal file
View File

@ -0,0 +1,33 @@
package models
import (
"errors"
"fmt"
. "github.com/go-jet/jet/v2/postgres"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
"github.com/google/uuid"
)
func SaveImage(userId string, imageName string, imageData []byte) error {
stmt := UserImages.INSERT(UserImages.UserID, UserImages.ImageName, UserImages.Image).VALUES(userId, imageName, imageData)
_, err := stmt.Exec(db)
return err
}
func GetImage(imageId string) (model.UserImages, error) {
id := uuid.MustParse(imageId)
stmt := UserImages.SELECT(UserImages.ImageName, UserImages.Image).WHERE(UserImages.ID.EQ(UUID(id)))
images := []model.UserImages{}
err := stmt.Query(db, &images)
if len(images) != 1 {
return model.UserImages{}, errors.New(fmt.Sprintf("Expected 1, got %d\n", len(images)))
}
return images[0], err
}

280
backend/openai.go Normal file
View File

@ -0,0 +1,280 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
)
type ImageInfo struct {
Tags []string `json:"tags"`
Text []string `json:"text"`
Links []string `json:"links"`
}
type ResponseFormat struct {
Type string `json:"type"`
JsonSchema any `json:"json_schema"`
}
type OpenAiRequestBody struct {
Model string `json:"model"`
Temperature float64 `json:"temperature"`
ResponseFormat ResponseFormat `json:"response_format"`
OpenAiMessages
}
type OpenAiMessages struct {
Messages []OpenAiMessage `json:"messages"`
}
type OpenAiMessage interface {
MessageToJson() ([]byte, error)
}
type OpenAiTextMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
func (textContent OpenAiTextMessage) MessageToJson() ([]byte, error) {
// TODO: Validate the `Role`.
return json.Marshal(textContent)
}
type OpenAiArrayMessage struct {
Role string `json:"role"`
Content []OpenAiContent `json:"content"`
}
func (arrayContent OpenAiArrayMessage) MessageToJson() ([]byte, error) {
return json.Marshal(arrayContent)
}
func (content *OpenAiMessages) AddImage(imageName string, image []byte) error {
extension := filepath.Ext(imageName)
if len(extension) == 0 {
// TODO: could also validate for image types we support.
return errors.New("Image does not have extension")
}
extension = extension[1:]
encodedString := base64.StdEncoding.EncodeToString(image)
arrayMessage := OpenAiArrayMessage{Role: ROLE_USER, Content: make([]OpenAiContent, 1)}
arrayMessage.Content[0] = OpenAiImage{
ImageType: IMAGE_TYPE,
ImageUrl: ImageUrl{
Url: fmt.Sprintf("data:image/%s;base64,%s", extension, encodedString),
},
}
content.Messages = append(content.Messages, arrayMessage)
return nil
}
func (content *OpenAiMessages) AddSystem(prompt string) error {
if len(content.Messages) != 0 {
return errors.New("You can only add a system prompt at the beginning")
}
content.Messages = append(content.Messages, OpenAiTextMessage{
Role: ROLE_SYSTEM,
Content: prompt,
})
return nil
}
type OpenAiContent interface {
ToJson() ([]byte, error)
}
type ImageUrl struct {
Url string `json:"url"`
}
type OpenAiImage struct {
ImageType string `json:"type"`
ImageUrl ImageUrl `json:"image_url"`
}
func (imageMessage OpenAiImage) ToJson() ([]byte, error) {
imageMessage.ImageType = IMAGE_TYPE
return json.Marshal(imageMessage)
}
type OpenAiClient struct {
url string
apiKey string
systemPrompt string
responseFormat string
Do func(req *http.Request) (*http.Response, error)
}
// func (client OpenAiClient) Do(req *http.Request) () {
// httpClient := http.Client{}
// return httpClient.Do(req)
// }
const OPENAI_API_KEY = "OPENAI_API_KEY"
const ROLE_USER = "user"
const ROLE_SYSTEM = "system"
const IMAGE_TYPE = "image_url"
// TODO: extract to text file probably
const PROMPT = `
You are an image information extractor. The user will provide you with screenshots and your job is to extract any relevant links and text
that the image might contain. You will also try your best to assign some tags to this image, avoid too many tags.
This system is part of a bookmark manager, who's main goal is to allow the user to search through various screenshots.
`
const RESPONSE_FORMAT = `
{
"name": "schema_description",
"schema": {
"type": "object",
"properties": {
"tags": {
"type": "array",
"description": "A list of tags you think the image is relevant to.",
"items": {
"type": "string"
}
},
"text": {
"type": "array",
"description": "A list of sentences the image contains.",
"items": {
"type": "string"
}
},
"links": {
"type": "array",
"description": "A list of all the links you can find in the image.",
"items": {
"type": "string"
}
}
},
"required": [
"tags",
"text",
"links"
],
"additionalProperties": false
},
"strict": true
}
`
func CreateOpenAiClient() (OpenAiClient, error) {
apiKey := os.Getenv(OPENAI_API_KEY)
if len(apiKey) == 0 {
return OpenAiClient{}, errors.New(OPENAI_API_KEY + " was not found.")
}
return OpenAiClient{
apiKey: apiKey,
url: "https://api.openai.com/v1/chat/completions",
systemPrompt: PROMPT,
Do: func(req *http.Request) (*http.Response, error) {
client := &http.Client{}
return client.Do(req)
},
}, nil
}
func (client OpenAiClient) getRequest(body []byte) (*http.Request, error) {
req, err := http.NewRequest("POST", client.url, bytes.NewBuffer(body))
if err != nil {
return req, err
}
req.Header.Add("Authorization", "Bearer "+client.apiKey)
req.Header.Add("Content-Type", "application/json")
return req, nil
}
func getCompletionsForImage(model string, temperature float64, prompt, imageName string, imageData []byte) (OpenAiRequestBody, error) {
request := OpenAiRequestBody{
Model: model,
Temperature: temperature,
}
// TODO: Add build pattern here that deals with errors in some internal state?
// I want a monad!!!
err := request.AddSystem(prompt)
if err != nil {
return request, err
}
err = request.AddImage(imageName, imageData)
if err != nil {
return request, err
}
return request, nil
}
func (client OpenAiClient) GetImageInfo(imageName string, imageData []byte) (ImageInfo, error) {
aiRequest, err := getCompletionsForImage("gpt-4o-mini", 1.0, client.systemPrompt, imageName, imageData)
if err != nil {
return ImageInfo{}, err
}
var jsonSchema any
err = json.Unmarshal([]byte(RESPONSE_FORMAT), &jsonSchema)
if err != nil {
return ImageInfo{}, err
}
aiRequest.ResponseFormat = ResponseFormat{
Type: "json_schema",
JsonSchema: jsonSchema,
}
jsonAiRequest, err := json.Marshal(aiRequest)
if err != nil {
return ImageInfo{}, err
}
request, err := client.getRequest(jsonAiRequest)
if err != nil {
return ImageInfo{}, err
}
resp, err := client.Do(request)
if err != nil {
return ImageInfo{}, err
}
response, err := io.ReadAll(resp.Body)
if err != nil {
return ImageInfo{}, err
}
info := ImageInfo{}
err = json.Unmarshal(response, &info)
if err != nil {
return ImageInfo{}, err
}
log.Println(string(response))
return info, nil
}

151
backend/openai_test.go Normal file
View File

@ -0,0 +1,151 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"testing"
)
func TestMessageBuilder(t *testing.T) {
content := OpenAiMessages{}
err := content.AddSystem("Some prompt")
if err != nil {
t.Log(err)
t.FailNow()
}
if len(content.Messages) != 1 {
t.Logf("Expected length 1, got %d.\n", len(content.Messages))
t.FailNow()
}
}
func TestMessageBuilderImage(t *testing.T) {
content := OpenAiMessages{}
prompt := "some prompt"
imageTitle := "image.png"
data := []byte("some data")
content.AddSystem(prompt)
content.AddImage(imageTitle, data)
if len(content.Messages) != 2 {
t.Logf("Expected length 2, got %d.\n", len(content.Messages))
t.FailNow()
}
promptMessage, ok := content.Messages[0].(OpenAiTextMessage)
if !ok {
t.Logf("Expected text content message, got %T\n", content.Messages[0])
t.FailNow()
}
if promptMessage.Role != ROLE_SYSTEM {
t.Log("Prompt message role is incorrect.")
t.FailNow()
}
if promptMessage.Content != prompt {
t.Log("Prompt message content is incorrect.")
t.FailNow()
}
arrayContentMessage, ok := content.Messages[1].(OpenAiArrayMessage)
if !ok {
t.Logf("Expected text content message, got %T\n", content.Messages[1])
t.FailNow()
}
if arrayContentMessage.Role != ROLE_USER {
t.Log("Array content message role is incorrect.")
t.FailNow()
}
if len(arrayContentMessage.Content) != 1 {
t.Logf("Expected length 1, got %d.\n", len(arrayContentMessage.Content))
t.FailNow()
}
imageContent, ok := arrayContentMessage.Content[0].(OpenAiImage)
if !ok {
t.Logf("Expected text content message, got %T\n", arrayContentMessage.Content[0])
t.FailNow()
}
base64data := base64.StdEncoding.EncodeToString(data)
url := fmt.Sprintf("data:image/%s;base64,%s", "png", base64data)
if imageContent.ImageUrl.Url != url {
t.Logf("Expected %s, but got %s.\n", url, imageContent.ImageUrl.Url)
t.FailNow()
}
}
func TestFullImageRequest(t *testing.T) {
request, err := getCompletionsForImage("model", 0.1, "You are an assistant", "image.png", []byte("some data"))
if err != nil {
t.Log(request)
t.FailNow()
}
jsonData, err := json.Marshal(request)
if err != nil {
t.Log(err)
t.FailNow()
}
expectedJson := `{"model":"model","temperature":0.1,"response_format":{"type":"","json_schema":""},"messages":[{"role":"system","content":"You are an assistant"},{"role":"user","content":[{"type":"image_url","image_url":{"url":"data:image/png;base64,c29tZSBkYXRh"}}]}]}`
if string(jsonData) != expectedJson {
t.Logf("Expected:\n%s\n Got:\n%s\n", expectedJson, string(jsonData))
t.FailNow()
}
}
func TestResponse(t *testing.T) {
testResponse := `{"tags": ["tag1", "tag2"], "text": ["text1"], "links": []}`
buffer := bytes.NewReader([]byte(testResponse))
body := io.NopCloser(buffer)
client := OpenAiClient{
url: "http://localhost:1234",
apiKey: "some-key",
Do: func(_req *http.Request) (*http.Response, error) {
return &http.Response{Body: body}, nil
},
}
info, err := client.GetImageInfo("image.png", []byte("some data"))
if err != nil {
t.Log(err)
t.FailNow()
}
if len(info.Tags) != 2 || len(info.Text) != 1 || len(info.Links) != 0 {
t.Logf("Some lengths are wrong.\nTags: %d\nText: %d\nLinks: %d\n", len(info.Tags), len(info.Text), len(info.Links))
t.FailNow()
}
if info.Tags[0] != "tag1" {
t.Log("0th tag is wrong.")
t.FailNow()
}
if info.Tags[1] != "tag2" {
t.Log("1th tag is wrong.")
t.FailNow()
}
if info.Text[0] != "text1" {
t.Log("0th text is wrong.")
t.FailNow()
}
}

56
backend/schema.sql Normal file
View File

@ -0,0 +1,56 @@
DROP SCHEMA IF EXISTS haystack CASCADE;
CREATE SCHEMA haystack;
/* -----| Schema tables |----- */
CREATE TABLE haystack.users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid()
);
CREATE TABLE haystack.user_images (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
image_name TEXT NOT NULL,
image BYTEA NOT NULL,
user_id uuid NOT NULL REFERENCES haystack.users (id)
);
CREATE TABLE haystack.user_tags (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tag TEXT NOT NULL,
user_id uuid NOT NULL REFERENCES haystack.users (id)
);
CREATE TABLE haystack.image_tags (
tag_id UUID NOT NULL REFERENCES haystack.user_tags (id),
image_id UUID NOT NULL REFERENCES haystack.user_images (id)
);
CREATE TABLE haystack.image_text (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
image_text TEXT NOT NULL,
image_id UUID NOT NULL REFERENCES haystack.user_images (id)
);
CREATE TABLE haystack.image_links (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
link TEXT NOT NULL,
image_id UUID NOT NULL REFERENCES haystack.user_images (id)
);
/* -----| Stored Procedures |----- */
CREATE OR REPLACE FUNCTION notify_new_image()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('new_image', NEW.id::texT);
RETURN NEW;
END
$$ LANGUAGE plpgsql;
/* -----| Triggers |----- */
CREATE OR REPLACE TRIGGER on_new_image AFTER INSERT
ON haystack.user_images
FOR EACH ROW
EXECUTE PROCEDURE notify_new_image()