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.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 {
return nil, fmt.Errorf("invalid iterations (1-50): %d", r.Iterations)
}

10
env.go
View File

@@ -21,6 +21,7 @@ type EnvTokens struct {
type EnvSettings struct {
CleanContent bool `json:"cleanup"`
TitleModel string `json:"title-model"`
ImageGeneration bool `json:"image-generation"`
}
type EnvUser struct {
@@ -46,6 +47,7 @@ var env = Environment{
// defaults
Settings: EnvSettings{
CleanContent: true,
ImageGeneration: true,
},
}
@@ -67,6 +69,13 @@ func (e *Environment) Init() error {
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
if e.Tokens.Secret == "" {
log.Warnln("Missing tokens.secret, generating new...")
@@ -135,6 +144,7 @@ func (e *Environment) Store() error {
"$.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.image-generation": {yaml.HeadComment(" allow image generation (optional; default: true)")},
"$.authentication.enabled": {yaml.HeadComment(" require login with username and password")},
"$.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/go-chi/chi/v5 v5.2.3
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
)

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.3 h1:ollIaPrgVWgqJyKbJGSX1jFs66eAWJs8Ojrxnd2i/E0=
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/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=

View File

@@ -18,6 +18,7 @@ type Model struct {
Vision bool `json:"-"`
JSON bool `json:"-"`
Tools bool `json:"-"`
Images bool `json:"-"`
}
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)
}

View File

@@ -363,6 +363,18 @@ body:not(.loading) #loading {
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.editing div.text {
display: none;
@@ -1256,7 +1268,7 @@ label[for="reasoning-tokens"] {
}
50% {
left: calc(100%);
left: 100%;
transform: translateX(-100%);
}
}

View File

@@ -111,6 +111,10 @@
background-image: url(icons/tags/json.svg)
}
.tags .tag.image {
background-image: url(icons/tags/image.svg)
}
.tags .tag.all {
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;
#state = false;
#loading = false;
#_diff;
#pending = {};
@@ -750,6 +751,16 @@
this.#save();
}
setLoading(loading) {
if (this.#loading === loading) {
return;
}
this.#loading = loading;
this.#_message.classList.toggle("loading", this.#loading);
}
setState(state) {
if (this.#state === state) {
return;
@@ -1032,7 +1043,21 @@
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) {
if (!message) {
@@ -1076,6 +1101,8 @@
signal: chatController.signal,
},
chunk => {
stopLoadingTimeout();
if (chunk === "aborted") {
chatController = null;
@@ -1135,6 +1162,8 @@
break;
}
startLoadingTimeout();
}
);
}

View File

@@ -52,6 +52,19 @@ func (s *Stream) Send(ch Chunk) error {
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 {
return Chunk{
Type: "reason",
@@ -73,10 +86,10 @@ func ToolChunk(tool *ToolCall) Chunk {
}
}
func IDChunk(id string) Chunk {
func ErrorChunk(err error) Chunk {
return Chunk{
Type: "id",
Text: id,
Type: "error",
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 {
if apiErr, ok := err.(*openrouter.APIError); ok {
return apiErr.Error()