Voice (dictate, read aloud) and Draw a picture over /v1, with the app's models scope
The server kept voice and images on /v1 (models scope, which the app's token already asks for); /me's services and scopes decide what is offered. Rust sends the recording to /v1/audio/transcriptions, reads /v1/audio/speech back as a data: URL (CSP media-src data:), and for /v1/images/generations takes only the id of each of the person's own files from the answer and fetches /v1/files/<id>/content from the app's own server. The microphone is allowed only within seconds of the page's own Dictate (voice_arm); WebKitGTK's media stream is switched on through its settings getter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
16 files changed
+1024
-4
No files matched your search
@@ -0,0 +1,86 @@
|
||||
// Draw a picture: the instance's own image generation (`POST /v1/images/generations`, its
|
||||
// workflow and default checkpoint), from Ctrl+K. Offered only where `GET /me` says the account may
|
||||
// (`services.images`) and the token holds `models`.
|
||||
//
|
||||
// The pictures are shown only as Rust read them: each one of the person's own files on this
|
||||
// server (`/v1/files/<id>/content`, fetched by id from the app's own server, never from an
|
||||
// address the answer named), handed over as a `data:` URL of an image type -- checked again here
|
||||
// before it becomes an <img>.
|
||||
|
||||
import { h, icon, ICONS } from "./dom.js"
|
||||
import { invoke, why } from "./bridge.js"
|
||||
|
||||
const PICTURE = /^data:image\/(png|jpeg|webp|gif);base64,[A-Za-z0-9+/=]+$/
|
||||
|
||||
/** Whether pictures can be drawn here, for the account `me` (`GET /me`). */
|
||||
export function canDraw(me) {
|
||||
const models = !Array.isArray(me?.scopes) || me.scopes.includes("models")
|
||||
return !!me?.services?.images && models && me?.permissions?.["tools.image"] !== false
|
||||
}
|
||||
|
||||
/** The dialog: a prompt, a shape, Draw, and the pictures under it. */
|
||||
export function openDraw() {
|
||||
const prompt = h("textarea", { class: "textarea draw__prompt", rows: "3", placeholder: t("What to draw…"), "aria-label": t("What to draw…") })
|
||||
const size = h(
|
||||
"select",
|
||||
{ class: "draw__size", "aria-label": t("Shape") },
|
||||
h("option", { value: "1024x1024", text: t("Square") }),
|
||||
h("option", { value: "1344x768", text: t("Wide") }),
|
||||
h("option", { value: "768x1344", text: t("Tall") }),
|
||||
)
|
||||
const status = h("p", { class: "status", role: "status", hidden: true })
|
||||
const out = h("div", { class: "draw__out" })
|
||||
const go = h("button", { type: "submit", class: "btn btn--primary", text: t("Draw") })
|
||||
const form = h(
|
||||
"form",
|
||||
{ class: "dialog__body draw", method: "dialog" },
|
||||
h("h2", { class: "dialog__title" }, icon(ICONS.picture), h("span", { text: t("Draw a picture") })),
|
||||
prompt,
|
||||
h("div", { class: "draw__bar" }, size, h("button", { type: "button", class: "btn", text: t("Close"), onclick: () => close() }), go),
|
||||
status,
|
||||
out,
|
||||
)
|
||||
const dlg = h("dialog", { class: "dialog dialog--wide" }, form)
|
||||
let asked = 0
|
||||
const close = () => {
|
||||
asked++
|
||||
dlg.close()
|
||||
dlg.remove()
|
||||
}
|
||||
const said = (text, error = false) => {
|
||||
status.hidden = !text
|
||||
status.textContent = text
|
||||
status.className = `status${error ? " is-error" : ""}`
|
||||
status.setAttribute("role", error ? "alert" : "status")
|
||||
}
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault()
|
||||
const words = prompt.value.trim()
|
||||
if (!words) return prompt.focus()
|
||||
const mine = ++asked
|
||||
go.disabled = true
|
||||
said(t("Drawing… a picture can take a minute or two."))
|
||||
try {
|
||||
const urls = await invoke("draw", { prompt: words, size: size.value })
|
||||
if (mine !== asked) return
|
||||
const pictures = (urls || []).filter((u) => PICTURE.test(u))
|
||||
if (!pictures.length) throw { detail: t("The server's answer had no picture in it.") }
|
||||
out.prepend(...pictures.map((src) => h("figure", { class: "draw__pic" }, h("img", { src, alt: words }), h("figcaption", { text: words }))))
|
||||
said("")
|
||||
} catch (err) {
|
||||
if (mine === asked) said(why(err), true)
|
||||
} finally {
|
||||
if (mine === asked) go.disabled = false
|
||||
}
|
||||
})
|
||||
prompt.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) form.requestSubmit()
|
||||
})
|
||||
dlg.addEventListener("cancel", (e) => {
|
||||
e.preventDefault()
|
||||
close()
|
||||
})
|
||||
document.body.append(dlg)
|
||||
dlg.showModal()
|
||||
prompt.focus()
|
||||
}
|
||||
Reference in new issue
Block a user