1
0
mirror of https://github.com/coalaura/whiskr.git synced 2025-12-02 20:22:52 +00:00

image generation wip

This commit is contained in:
Laura
2025-09-11 23:25:58 +02:00
parent 27dbd0e642
commit 1c4ff26378
10 changed files with 105 additions and 24 deletions

View File

@@ -105,6 +105,14 @@ func (r *Request) Parse() (*openrouter.ChatCompletionRequest, error) {
request.Model = r.Model request.Model = r.Model
request.Modalities = []openrouter.ChatCompletionModality{
openrouter.ModalityText,
}
if env.Settings.ImageGeneration && model.Images {
request.Modalities = append(request.Modalities, openrouter.ModalityImage)
}
if r.Iterations < 1 || r.Iterations > 50 { if r.Iterations < 1 || r.Iterations > 50 {
return nil, fmt.Errorf("invalid iterations (1-50): %d", r.Iterations) return nil, fmt.Errorf("invalid iterations (1-50): %d", r.Iterations)
} }

20
env.go
View File

@@ -19,8 +19,9 @@ type EnvTokens struct {
} }
type EnvSettings struct { type EnvSettings struct {
CleanContent bool `json:"cleanup"` CleanContent bool `json:"cleanup"`
TitleModel string `json:"title-model"` TitleModel string `json:"title-model"`
ImageGeneration bool `json:"image-generation"`
} }
type EnvUser struct { type EnvUser struct {
@@ -45,7 +46,8 @@ type Environment struct {
var env = Environment{ var env = Environment{
// defaults // defaults
Settings: EnvSettings{ Settings: EnvSettings{
CleanContent: true, CleanContent: true,
ImageGeneration: true,
}, },
} }
@@ -67,6 +69,13 @@ func (e *Environment) Init() error {
log.Warnln("Debug mode enabled") log.Warnln("Debug mode enabled")
} }
// print if image generation is enabled
if e.Settings.ImageGeneration {
log.Warnln("Image generation enabled")
} else {
log.Warnln("Image generation disabled")
}
// check if server secret is set // check if server secret is set
if e.Tokens.Secret == "" { if e.Tokens.Secret == "" {
log.Warnln("Missing tokens.secret, generating new...") log.Warnln("Missing tokens.secret, generating new...")
@@ -133,8 +142,9 @@ func (e *Environment) Store() error {
"$.tokens.exa": {yaml.HeadComment(" exa search api token (optional; used by search tools)")}, "$.tokens.exa": {yaml.HeadComment(" exa search api token (optional; used by search tools)")},
"$.tokens.github": {yaml.HeadComment(" github api token (optional; used by search tools)")}, "$.tokens.github": {yaml.HeadComment(" github api token (optional; used by search tools)")},
"$.settings.cleanup": {yaml.HeadComment(" normalize unicode in assistant output (optional; default: true)")}, "$.settings.cleanup": {yaml.HeadComment(" normalize unicode in assistant output (optional; default: true)")},
"$.settings.title-model": {yaml.HeadComment(" model used to generate titles (needs to have structured output support; default: google/gemini-2.5-flash-lite)")}, "$.settings.title-model": {yaml.HeadComment(" model used to generate titles (needs to have structured output support; default: google/gemini-2.5-flash-lite)")},
"$.settings.image-generation": {yaml.HeadComment(" allow image generation (optional; default: true)")},
"$.authentication.enabled": {yaml.HeadComment(" require login with username and password")}, "$.authentication.enabled": {yaml.HeadComment(" require login with username and password")},
"$.authentication.users": {yaml.HeadComment(" list of users with bcrypt password hashes")}, "$.authentication.users": {yaml.HeadComment(" list of users with bcrypt password hashes")},

2
go.mod
View File

@@ -6,7 +6,7 @@ require (
github.com/coalaura/plain v0.2.0 github.com/coalaura/plain v0.2.0
github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/chi/v5 v5.2.3
github.com/goccy/go-yaml v1.18.0 github.com/goccy/go-yaml v1.18.0
github.com/revrost/go-openrouter v0.2.3 github.com/revrost/go-openrouter v0.2.4-0.20250909110314-b8c4ee4c5861
golang.org/x/crypto v0.42.0 golang.org/x/crypto v0.42.0
) )

2
go.sum
View File

@@ -26,6 +26,8 @@ github.com/revrost/go-openrouter v0.2.2 h1:7bOdLPKmw0iJB1AdpN+YaWUd2XC9cwfJKDY10
github.com/revrost/go-openrouter v0.2.2/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0= github.com/revrost/go-openrouter v0.2.2/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
github.com/revrost/go-openrouter v0.2.3 h1:ollIaPrgVWgqJyKbJGSX1jFs66eAWJs8Ojrxnd2i/E0= github.com/revrost/go-openrouter v0.2.3 h1:ollIaPrgVWgqJyKbJGSX1jFs66eAWJs8Ojrxnd2i/E0=
github.com/revrost/go-openrouter v0.2.3/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0= github.com/revrost/go-openrouter v0.2.3/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
github.com/revrost/go-openrouter v0.2.4-0.20250909110314-b8c4ee4c5861 h1:4XU64nIgj6l9659KJx+FOaABvdhM3YrytCgD8XoKu90=
github.com/revrost/go-openrouter v0.2.4-0.20250909110314-b8c4ee4c5861/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=

View File

@@ -18,6 +18,7 @@ type Model struct {
Vision bool `json:"-"` Vision bool `json:"-"`
JSON bool `json:"-"` JSON bool `json:"-"`
Tools bool `json:"-"` Tools bool `json:"-"`
Images bool `json:"-"`
} }
var ModelMap = make(map[string]*Model) var ModelMap = make(map[string]*Model)
@@ -89,5 +90,13 @@ func GetModelTags(model openrouter.Model, m *Model) {
} }
} }
for _, modality := range model.Architecture.OutputModalities {
if modality == "image" {
m.Images = true
m.Tags = append(m.Tags, "image")
}
}
sort.Strings(m.Tags) sort.Strings(m.Tags)
} }

View File

@@ -363,6 +363,18 @@ body:not(.loading) #loading {
max-width: 800px; max-width: 800px;
} }
.message.loading .text::after {
content: "";
display: block;
position: relative;
width: 32px;
height: 3px;
background: #cad3f5;
animation: swivel 1.5s ease-in-out infinite;
opacity: 0.5;
margin-top: 5px;
}
.message:not(.editing) textarea.text, .message:not(.editing) textarea.text,
.message.editing div.text { .message.editing div.text {
display: none; display: none;
@@ -1256,7 +1268,7 @@ label[for="reasoning-tokens"] {
} }
50% { 50% {
left: calc(100%); left: 100%;
transform: translateX(-100%); transform: translateX(-100%);
} }
} }

View File

@@ -111,6 +111,10 @@
background-image: url(icons/tags/json.svg) background-image: url(icons/tags/json.svg)
} }
.tags .tag.image {
background-image: url(icons/tags/image.svg)
}
.tags .tag.all { .tags .tag.all {
background-image: url(icons/tags/all.svg) background-image: url(icons/tags/all.svg)
} }

View File

@@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -146,6 +146,7 @@
#editing = false; #editing = false;
#state = false; #state = false;
#loading = false;
#_diff; #_diff;
#pending = {}; #pending = {};
@@ -750,6 +751,16 @@
this.#save(); this.#save();
} }
setLoading(loading) {
if (this.#loading === loading) {
return;
}
this.#loading = loading;
this.#_message.classList.toggle("loading", this.#loading);
}
setState(state) { setState(state) {
if (this.#state === state) { if (this.#state === state) {
return; return;
@@ -1032,7 +1043,21 @@
messages: messages.map(message => message.getData()).filter(Boolean), messages: messages.map(message => message.getData()).filter(Boolean),
}; };
let message, generationID; let message, generationID, timeout;
function stopLoadingTimeout() {
clearTimeout(timeout);
message?.setLoading(false);
}
function startLoadingTimeout() {
clearTimeout(timeout);
timeout = setTimeout(() => {
message?.setLoading(true);
}, 1500);
}
function finish(aborted = false) { function finish(aborted = false) {
if (!message) { if (!message) {
@@ -1076,6 +1101,8 @@
signal: chatController.signal, signal: chatController.signal,
}, },
chunk => { chunk => {
stopLoadingTimeout();
if (chunk === "aborted") { if (chunk === "aborted") {
chatController = null; chatController = null;
@@ -1135,6 +1162,8 @@
break; break;
} }
startLoadingTimeout();
} }
); );
} }

View File

@@ -52,6 +52,19 @@ func (s *Stream) Send(ch Chunk) error {
return WriteChunk(s.wr, s.ctx, ch) return WriteChunk(s.wr, s.ctx, ch)
} }
func StartChunk() Chunk {
return Chunk{
Type: "start",
}
}
func IDChunk(id string) Chunk {
return Chunk{
Type: "id",
Text: id,
}
}
func ReasoningChunk(text string) Chunk { func ReasoningChunk(text string) Chunk {
return Chunk{ return Chunk{
Type: "reason", Type: "reason",
@@ -73,10 +86,10 @@ func ToolChunk(tool *ToolCall) Chunk {
} }
} }
func IDChunk(id string) Chunk { func ErrorChunk(err error) Chunk {
return Chunk{ return Chunk{
Type: "id", Type: "error",
Text: id, Text: GetErrorMessage(err),
} }
} }
@@ -86,19 +99,6 @@ func EndChunk() Chunk {
} }
} }
func StartChunk() Chunk {
return Chunk{
Type: "start",
}
}
func ErrorChunk(err error) Chunk {
return Chunk{
Type: "error",
Text: GetErrorMessage(err),
}
}
func GetErrorMessage(err error) string { func GetErrorMessage(err error) string {
if apiErr, ok := err.(*openrouter.APIError); ok { if apiErr, ok := err.(*openrouter.APIError); ok {
return apiErr.Error() return apiErr.Error()