-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 86fd187
Showing
14 changed files
with
774 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
tmp/ | ||
.air.toml | ||
sprite | ||
config.json* | ||
*.exe |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2023 Benny Zhao | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# desktop-AI-buddy | ||
|
||
Desktop mascot written in Golang | ||
Makes random remarks and responds to current desktop happenings using LLM (desktop screenshot capture) | ||
|
||
## Configuration | ||
Run binary once to generate a config file, then fill a [OpenRouter](https://openrouter.ai/) API key (optional, required if using ai features) | ||
|
||
## Credits | ||
[Pink monster sprite](https://free-game-assets.itch.io/free-tiny-hero-sprites-pixel-art) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
module desktop-ai-buddy | ||
|
||
go 1.21.5 | ||
|
||
require ( | ||
github.com/hajimehoshi/ebiten/v2 v2.6.3 | ||
github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237 | ||
github.com/sashabaranov/go-openai v1.17.9 | ||
) | ||
|
||
require ( | ||
github.com/ebitengine/purego v0.5.0 // indirect | ||
github.com/fogleman/gg v1.3.0 | ||
github.com/gen2brain/shm v0.0.0-20230802011745-f2460f5984f7 // indirect | ||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect | ||
github.com/jezek/xgb v1.1.0 // indirect | ||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect | ||
golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63 // indirect | ||
golang.org/x/image v0.12.0 | ||
golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57 // indirect | ||
golang.org/x/sync v0.3.0 // indirect | ||
golang.org/x/sys v0.12.0 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
github.com/ebitengine/purego v0.5.0 h1:JrMGKfRIAM4/QVKaesIIT7m/UVjTj5GYhRSQYwfVdpo= | ||
github.com/ebitengine/purego v0.5.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= | ||
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= | ||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= | ||
github.com/gen2brain/shm v0.0.0-20230802011745-f2460f5984f7 h1:VLEKvjGJYAMCXw0/32r9io61tEXnMWDRxMk+peyRVFc= | ||
github.com/gen2brain/shm v0.0.0-20230802011745-f2460f5984f7/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo= | ||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= | ||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= | ||
github.com/hajimehoshi/ebiten/v2 v2.6.3 h1:xJ5klESxhflZbPUx3GdIPoITzgPgamsyv8aZCVguXGI= | ||
github.com/hajimehoshi/ebiten/v2 v2.6.3/go.mod h1:TZtorL713an00UW4LyvMeKD8uXWnuIuCPtlH11b0pgI= | ||
github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk= | ||
github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= | ||
github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237 h1:YOp8St+CM/AQ9Vp4XYm4272E77MptJDHkwypQHIRl9Q= | ||
github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237/go.mod h1:e7qQlOY68wOz4b82D7n+DdaptZAi+SHW0+yKiWZzEYE= | ||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= | ||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= | ||
github.com/sashabaranov/go-openai v1.17.9 h1:QEoBiGKWW68W79YIfXWEFZ7l5cEgZBV4/Ow3uy+5hNY= | ||
github.com/sashabaranov/go-openai v1.17.9/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= | ||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | ||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||
golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63 h1:3AGKexOYqL+ztdWdkB1bDwXgPBuTS/S8A4WzuTvJ8Cg= | ||
golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0= | ||
golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ= | ||
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk= | ||
golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57 h1:Q6NT8ckDYNcwmi/bmxe+XbiDMXqMRW1xFBtJ+bIpie4= | ||
golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57/go.mod h1:wEyOn6VvNW7tcf+bW/wBz1sehi2s2BZ4TimyR7qZen4= | ||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | ||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= | ||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= | ||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= | ||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= | ||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= | ||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= | ||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
package main | ||
|
||
import ( | ||
"image" | ||
_ "image/png" | ||
"log" | ||
"math/rand" | ||
"time" | ||
|
||
"github.com/hajimehoshi/ebiten/v2" | ||
"github.com/hajimehoshi/ebiten/v2/inpututil" | ||
"github.com/kbinani/screenshot" | ||
) | ||
|
||
const ( | ||
windowWidth = 128 | ||
windowHeight = 128 | ||
) | ||
|
||
func (g *Game) decideNewAction() Action { | ||
if g.isTalking { | ||
if g.windowPos.X > 960 { | ||
return g.idleLeftAction | ||
} else { | ||
return g.idleRightAction | ||
} | ||
} | ||
|
||
randomPercent := rand.Float64() | ||
walkProbability := 0.25 | ||
pointRight := rand.Float64() < 0.5 | ||
|
||
switch { | ||
case randomPercent < walkProbability: | ||
if pointRight { | ||
return g.walkRightAction | ||
} | ||
return g.walkLeftAction | ||
default: | ||
switch g.curAction.name { | ||
case "walkRight": | ||
return g.idleRightAction | ||
case "walkLeft": | ||
return g.idleLeftAction | ||
default: | ||
if pointRight { | ||
return g.idleRightAction | ||
} | ||
return g.idleLeftAction | ||
} | ||
} | ||
} | ||
|
||
func (g *Game) decideMessage() { | ||
randomPercent := rand.Float64() | ||
screenProbability := 0.025 | ||
talkProbability := 0.1 | ||
|
||
switch { | ||
case randomPercent < screenProbability: | ||
image, error := screenshot.CaptureDisplay(0) | ||
if error != nil { | ||
g.isTalking = false | ||
return | ||
} | ||
log.Println("Requesting comment on screenshot") | ||
msg, err := requestImageComment(*g.openaiClient, image) | ||
if err != nil { | ||
g.isTalking = false | ||
return | ||
} | ||
g.speechBubble = ebiten.NewImageFromImage(create_text_bubble(msg)) | ||
g.isTalking = true | ||
|
||
case randomPercent < screenProbability+talkProbability: | ||
currentTime := time.Now() | ||
timeString := currentTime.Format("15:04") | ||
log.Println("Requesting chat") | ||
msg, err := requestChat(*g.openaiClient, "The time is "+timeString+"."+" Give a cheerful short remark to the user") | ||
if err != nil { | ||
g.isTalking = false | ||
return | ||
} | ||
g.speechBubble = ebiten.NewImageFromImage(create_text_bubble(msg)) | ||
g.isTalking = true | ||
default: | ||
g.isTalking = false | ||
} | ||
|
||
} | ||
|
||
func (g *Game) Update() error { | ||
g.updateTick++ | ||
|
||
isLeftClick := ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) | ||
if isLeftClick { | ||
g.moveWindow() | ||
} else if g.curAction.name == "gettingDragged" { | ||
g.curAction = g.decideNewAction() | ||
} else if g.updateTick%200 == 0 { | ||
g.curAction = g.decideNewAction() | ||
} | ||
if g.updateTick%500 == 0 && !g.isTalking && g.openaiClient != nil { | ||
g.speechBubble = nil | ||
g.isTalking = true | ||
go g.decideMessage() | ||
} else if g.isTalking && (ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) || g.updateTick%2000 == 0) { | ||
g.isTalking = false | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (g *Game) Draw(screen *ebiten.Image) { | ||
g.drawTick++ | ||
|
||
ebiten.SetWindowPosition(g.windowPos.X, g.windowPos.Y) | ||
|
||
x, y := ebiten.WindowSize() | ||
op := &ebiten.DrawImageOptions{} | ||
if g.isTalking && g.speechBubble != nil { | ||
|
||
bounds := g.speechBubble.Bounds().Size() | ||
if (bounds.X+50) > x && y > -1 { | ||
ebiten.SetWindowSize((windowWidth)+(bounds.X)+25, windowHeight) | ||
} | ||
x, y = ebiten.WindowSize() | ||
if bounds.Y > y { | ||
ebiten.SetWindowSize(x, (windowHeight/2)+(bounds.Y)) | ||
} | ||
|
||
op.GeoM.Translate(100, 0) | ||
screen.DrawImage(g.speechBubble, op) | ||
|
||
} else if x != windowWidth || y != windowHeight { | ||
ebiten.SetWindowSize(windowWidth, windowHeight) | ||
} | ||
|
||
currSheet := g.curAction.animation.sprite | ||
frameHeight := g.curAction.animation.frameHeight | ||
frameWidth := g.curAction.animation.frameWidth | ||
frameCount := g.curAction.animation.frameCount | ||
speed := g.curAction.animation.speed | ||
|
||
op = &ebiten.DrawImageOptions{} | ||
op.GeoM.Scale(3, 3) | ||
op.GeoM.Translate(18, float64(windowHeight/2)-float64(frameHeight*3/2)) | ||
i := (g.drawTick / speed) % frameCount | ||
sx := i * frameWidth | ||
|
||
frame := currSheet.SubImage(image.Rect(sx, 0, sx+frameWidth, frameHeight)).(*ebiten.Image) | ||
screen.DrawImage(frame, op) | ||
|
||
g.updateCharacterPosition() | ||
} | ||
|
||
func (g *Game) updateCharacterPosition() { | ||
x, _ := ebiten.WindowSize() | ||
if g.curAction.name == "walkRight" { | ||
if g.windowPos.X+x > 1820 { | ||
g.curAction = g.walkLeftAction | ||
g.windowPos.X -= 1 | ||
} else { | ||
g.windowPos.X += 1 | ||
} | ||
} else if g.curAction.name == "walkLeft" { | ||
if g.windowPos.X-x < 100 { | ||
g.curAction = g.walkRightAction | ||
g.windowPos.X += 1 | ||
} else { | ||
g.windowPos.X -= 1 | ||
} | ||
} | ||
} | ||
|
||
func (g *Game) Layout(outsideWidth, outsideHeight int) (windowWidth, windowHeight int) { | ||
return outsideWidth, outsideHeight | ||
} | ||
|
||
func (g *Game) moveWindow() { | ||
cursorX, cursorY := ebiten.CursorPosition() | ||
cursorPos := Point{X: cursorX, Y: cursorY} | ||
g.curAction = g.gettingDraggedAction | ||
|
||
initalPress := inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) | ||
if initalPress { | ||
g.initalLeftPress = cursorPos | ||
} | ||
|
||
currX, currY := ebiten.WindowPosition() | ||
g.windowPos.X = (cursorPos.X + currX) - g.initalLeftPress.X | ||
g.windowPos.Y = (cursorPos.Y + currY) - g.initalLeftPress.Y | ||
|
||
if !(g.windowPos.X < 0) { | ||
return | ||
} | ||
|
||
monitors := ebiten.AppendMonitors(nil) | ||
curr := ebiten.Monitor() | ||
prevIndex := -1 | ||
|
||
for i, monitor := range monitors { | ||
if monitor == curr { | ||
prevIndex = i - 1 | ||
break | ||
} | ||
} | ||
|
||
if prevIndex >= 0 { | ||
ebiten.SetMonitor(monitors[prevIndex]) | ||
} | ||
} |
Oops, something went wrong.