mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 09:08:54 -04:00
feat(addons): ship Studio and Talk as add-ons instead of built-in pages
Studio and Talk were compiled into the web bundle and always listed in the Add-ons drawer. They are add-ons now, like Voice, Face, fine-tuning and quantization: a person installs and removes them, the drawer lists them only while they are installed, and they are packaged the same way. Move the pages, their components, hooks and helpers into addons/studio and addons/talk. Each has a manifest, a UI bundle, a release platform matrix and a catalog template, and is built by the compat mode of the shared builder. The core API stays where it is: the pages call it with the signed-in person's session. The history keeps its browser storage keys. The unit tests of the moved helpers move with them. In the core, drop the two pages, the Create group of the sidebar (Chat joins Home and Models in the old layout), the built-in catalog entries (the catalog is empty) and the talk locale namespace. The old addresses (/app/studio, /app/talk and the generator links) redirect to the add-on pages, with the model as ?model=. The Home /studio command is offered only while the Studio add-on is installed and opens it. The release workflow and its tests now list seven add-ons. Assisted-by: Claude Code:claude-sonnet-5-5 go node Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
119 files changed
+3401
-164
No files matched your search
@@ -56,7 +56,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
addon: [tables, voice, face, fine-tuning, quantization]
|
||||
addon: [tables, voice, face, fine-tuning, quantization, studio, talk]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
# event it fails the plan. A missing or malformed platform file, or
|
||||
# any other error, fails the plan on every event.
|
||||
include='[]'
|
||||
for id in tables voice face fine-tuning quantization; do
|
||||
for id in tables voice face fine-tuning quantization studio talk; do
|
||||
cfg="addons/$id/package/platforms.yaml"
|
||||
err="matrix-$id.err"
|
||||
if rows=$(go run ./scripts/addons/cmd/addonpkg matrix --config "$cfg" 2>"$err"); then
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Command studio is the executable of the Studio add-on. It serves no tools:
|
||||
// the add-on works in the browser, through the SDK and the core API.
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAI/addons/internal/idle"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(idle.Run(os.Args))
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "studio",
|
||||
"version": "0.1.0",
|
||||
"executable": "studio",
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"host": { "min": 1, "max": 1 },
|
||||
"api": { "min": 1, "max": 1 },
|
||||
"protocol": { "min": 1, "max": 1 },
|
||||
"dataFormat": { "min": 1, "max": 1 },
|
||||
"capabilities": {
|
||||
"ui": {
|
||||
"entry": "ui/studio.js",
|
||||
"css": ["ui/studio.css"],
|
||||
"sdk": { "min": 2, "max": 2 },
|
||||
"pages": ["studio"],
|
||||
"navigation": ["studio"],
|
||||
"mainPage": "studio",
|
||||
"icon": "ui/icon.png",
|
||||
"title": "Studio",
|
||||
"description": "Make images, video, 3D models, speech, sound and transformed audio with the models you have installed, and keep what you make."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# Release platform matrix of the studio add-on package.
|
||||
#
|
||||
# A platform ships only while "enabled" is true. Enable a platform in the same
|
||||
# change that shows its native check passing on a runner of that platform;
|
||||
# until then it stays listed and disabled. The release workflow fails closed
|
||||
# when no platform is enabled, and it never builds a platform that is not
|
||||
# enabled here.
|
||||
#
|
||||
# runner: the GitHub runner label that builds and checks the platform
|
||||
# natively. Required for an enabled platform.
|
||||
# native_check: "executable-start": the packaged executable is extracted on
|
||||
# the native runner and started. Required for an enabled platform.
|
||||
# runtime: files besides the executable that the platform needs at run
|
||||
# time, relative to the package root. They are taken from the
|
||||
# runtime directory of the build and must exist.
|
||||
schema: 1
|
||||
addon: studio
|
||||
executable: studio
|
||||
manifest: addons/studio/manifest.json
|
||||
# The directory whose content is laid at the package root (ui/studio.js ...).
|
||||
ui: addons/studio/ui/dist
|
||||
platforms:
|
||||
- platform: linux/amd64
|
||||
enabled: true
|
||||
runner: ubuntu-24.04
|
||||
native_check: executable-start
|
||||
- platform: linux/arm64
|
||||
enabled: true
|
||||
runner: ubuntu-24.04-arm
|
||||
native_check: executable-start
|
||||
- platform: darwin/arm64
|
||||
enabled: true
|
||||
runner: macos-14
|
||||
native_check: executable-start
|
||||
- platform: darwin/amd64
|
||||
enabled: true
|
||||
runner: macos-15-intel
|
||||
native_check: executable-start
|
||||
@@ -0,0 +1,2 @@
|
||||
# Written by build.mjs; the package build makes it.
|
||||
/dist/
|
||||
@@ -0,0 +1,15 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { buildAddonUI } from '../../internal/uibuild/uibuild.mjs'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export async function build() {
|
||||
// The page code keeps the host module names it was written with; the compat
|
||||
// directory answers them through the SDK (addons/internal/host-compat).
|
||||
return buildAddonUI({ root: here, name: 'studio', rgb: [150, 52, 92], compat: resolve(here, '../../internal/host-compat/src') })
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
build().then((dir) => console.log(`built ${dir}`), (e) => { console.error(e); process.exit(1) })
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "localai-studio-ui",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "Native UI of the Studio add-on: a standalone ES module that the host loads after its own build.",
|
||||
"type": "module",
|
||||
"scripts": { "build": "node build.mjs", "test": "node test/run.mjs" }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { RouteProvider } from 'react-router-dom'
|
||||
import { registerLocales } from '../../../internal/host-compat/src/i18n.js'
|
||||
import Studio from './pages/Studio'
|
||||
import en from './i18n/media.en.json'
|
||||
import it from './i18n/media.it.json'
|
||||
import es from './i18n/media.es.json'
|
||||
import de from './i18n/media.de.json'
|
||||
import zh from './i18n/media.zh-CN.json'
|
||||
import id from './i18n/media.id.json'
|
||||
import ko from './i18n/media.ko.json'
|
||||
import pt from './i18n/media.pt-BR.json'
|
||||
|
||||
// The words of the pages are the host's own `media` locale files.
|
||||
registerLocales({ media: { en, it, es, de, 'zh-CN': zh, id, ko, 'pt-BR': pt } })
|
||||
|
||||
export function createStudio() {
|
||||
// The shell passes the page, the rest of the path (the tab) and the query.
|
||||
function Component({ page, path, search }) {
|
||||
if (page !== 'studio') return null
|
||||
return (
|
||||
<RouteProvider value={{ page, path: path || '', search: search || '' }}>
|
||||
<Studio />
|
||||
</RouteProvider>
|
||||
)
|
||||
}
|
||||
return { Component, components: {}, chatActions: [], dispose() {} }
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getSharedAudioContext } from './useAudioPeaks'
|
||||
import { fftRadix2 } from '../utils/fft'
|
||||
import { fetchUrl } from '../utils/api'
|
||||
|
||||
// Hann windows are reused across frames and across clips, so cache one per
|
||||
// size. The window tapers each frame to suppress spectral leakage (the
|
||||
@@ -42,7 +43,7 @@ export default function useSpectrogram(
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const resp = await fetch(src)
|
||||
const resp = await fetchUrl(src)
|
||||
const raw = await resp.arrayBuffer()
|
||||
const ctx = getSharedAudioContext()
|
||||
if (!ctx) throw new Error('Web Audio API not available')
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { readAllMediaHistory } from './useMediaHistory'
|
||||
import { apiUrl } from '../utils/basePath'
|
||||
import { fetchUrl } from '../utils/api'
|
||||
import { collectWork, readHandoff, sourceUrlFor } from '../utils/studioWork'
|
||||
|
||||
// What the Studio front page hands to a workspace: a prompt, a model, a size, a
|
||||
@@ -38,7 +38,7 @@ export function useHandoffSource(handoff, enabled = true) {
|
||||
if (!item || !url) { setState({ status: 'missing', blob: null, item }); return undefined }
|
||||
let cancelled = false
|
||||
setState({ status: 'loading', blob: null, item })
|
||||
fetch(url.startsWith('http') || url.startsWith('data:') ? url : apiUrl(url))
|
||||
fetchUrl(url)
|
||||
.then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.blob() })
|
||||
.then(blob => { if (!cancelled) setState({ status: 'ready', blob, item }) })
|
||||
.catch(() => { if (!cancelled) setState({ status: 'error', blob: null, item }) })
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"studio": {
|
||||
"tabs": {
|
||||
"images": "Bilder",
|
||||
"video": "Video",
|
||||
"tts": "TTS",
|
||||
"sound": "Audio",
|
||||
"transform": "Transform",
|
||||
"overview": "Overview"
|
||||
},
|
||||
"overview": {
|
||||
"eyebrow": "{{ready}} of {{total}} modalities ready",
|
||||
"title": "Studio",
|
||||
"subtitle": "Generate images, video, 3D, speech and sound with the models on this machine.",
|
||||
"canMake": "What you can make",
|
||||
"running": "Running now",
|
||||
"recent": "Recent outputs",
|
||||
"noModel": "No model installed",
|
||||
"install": "Install a model",
|
||||
"ready": "Ready",
|
||||
"seconds": "{{seconds}}s",
|
||||
"describe": {
|
||||
"images": "Text to image, image to image, reference images",
|
||||
"video": "Text to video and image to video",
|
||||
"threed": "Image to mesh reconstruction",
|
||||
"tts": "Text to speech using your voice library",
|
||||
"sound": "Music and sound effects from a prompt",
|
||||
"transform": "Separation, enhancement and voice conversion"
|
||||
}
|
||||
},
|
||||
"groups": {
|
||||
"create": "Create",
|
||||
"voice": "Voice",
|
||||
"transform": "Transform"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"title": "Bildgenerierung",
|
||||
"labels": {
|
||||
"model": "Modell",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Beschreiben Sie das Bild, das Sie generieren möchten...",
|
||||
"negativePrompt": "Negativer Prompt",
|
||||
"negativePromptPlaceholder": "Was vermieden werden soll...",
|
||||
"size": "Größe",
|
||||
"count": "Anzahl (1-4)",
|
||||
"advanced": "Erweiterte Einstellungen",
|
||||
"imageInputs": "Bildeingaben",
|
||||
"steps": "Schritte",
|
||||
"stepsPlaceholder": "20",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Zufällig",
|
||||
"sourceImage": "Ausgangsbild (img2img)",
|
||||
"refImages": "Referenzbilder",
|
||||
"refImagesAdded_one": "{{count}} Bild hinzugefügt",
|
||||
"refImagesAdded_other": "{{count}} Bilder hinzugefügt"
|
||||
},
|
||||
"actions": {
|
||||
"view": "View",
|
||||
"generate": "Generieren",
|
||||
"generating": "Generieren..."
|
||||
},
|
||||
"empty": "Generierte Bilder erscheinen hier",
|
||||
"toasts": {
|
||||
"noPrompt": "Bitte einen Prompt eingeben",
|
||||
"noModel": "Bitte ein Modell auswählen",
|
||||
"noResults": "Keine Bilder generiert"
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"title": "Videogenerierung",
|
||||
"labels": {
|
||||
"model": "Modell",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Beschreiben Sie das Video, das Sie generieren möchten...",
|
||||
"duration": "Dauer (s)",
|
||||
"fps": "FPS",
|
||||
"size": "Größe",
|
||||
"advanced": "Erweiterte Einstellungen",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Zufällig",
|
||||
"frames": "Frames",
|
||||
"referenceMedia": "Referenzmedien",
|
||||
"startImage": "Startbild",
|
||||
"endImage": "Endbild",
|
||||
"avatarAudio": "Avatar-Audio"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Video generieren",
|
||||
"generating": "Generieren..."
|
||||
},
|
||||
"empty": "Generierte Videos erscheinen hier",
|
||||
"toasts": {
|
||||
"noPrompt": "Bitte einen Prompt eingeben",
|
||||
"noModel": "Bitte ein Modell auswählen",
|
||||
"noResults": "Keine Videos generiert"
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"title": "Text zu Sprache",
|
||||
"labels": {
|
||||
"model": "Modell",
|
||||
"voice": "Stimme",
|
||||
"voicePlaceholder": "Optionale Stimm-ID",
|
||||
"input": "Text",
|
||||
"inputPlaceholder": "Text zum Synthetisieren eingeben...",
|
||||
"instructions": "Anweisungen",
|
||||
"instructionsPlaceholder": "Zum Beispiel: Langsam und warmherzig sprechen",
|
||||
"instructionsHint": "Optionale Hinweise zur Sprechweise. Die Unterstützung hängt vom Backend ab."
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Audio generieren",
|
||||
"generating": "Generieren..."
|
||||
},
|
||||
"empty": "Generiertes Audio erscheint hier",
|
||||
"toasts": {
|
||||
"noText": "Bitte Text eingeben",
|
||||
"noModel": "Bitte ein Modell auswählen",
|
||||
"generateFailed": "Generierung fehlgeschlagen"
|
||||
}
|
||||
},
|
||||
"sound": {
|
||||
"title": "Audiogenerierung",
|
||||
"labels": {
|
||||
"model": "Modell",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Beschreiben Sie den Klang, den Sie generieren möchten...",
|
||||
"duration": "Dauer (s)",
|
||||
"language": "Sprache",
|
||||
"vocalLanguage": "Vokalsprache",
|
||||
"lyrics": "Liedtext (optional)",
|
||||
"lyricsPlaceholder": "Liedtext für Vokalgenerierung",
|
||||
"advanced": "Erweiterte Einstellungen",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Zufällig"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generieren",
|
||||
"generating": "Generieren..."
|
||||
},
|
||||
"empty": "Generiertes Audio erscheint hier",
|
||||
"toasts": {
|
||||
"noPrompt": "Bitte einen Prompt eingeben",
|
||||
"noModel": "Bitte ein Modell auswählen",
|
||||
"generateFailed": "Generierung fehlgeschlagen"
|
||||
}
|
||||
},
|
||||
"talk": {
|
||||
"title": "Sprechen",
|
||||
"subtitle": "Sprachunterhaltung in Echtzeit",
|
||||
"actions": {
|
||||
"start": "Sitzung starten",
|
||||
"stop": "Sitzung beenden",
|
||||
"connecting": "Verbinden...",
|
||||
"muted": "Stumm",
|
||||
"mute": "Stummschalten",
|
||||
"unmute": "Stumm aufheben"
|
||||
},
|
||||
"labels": {
|
||||
"model": "Modell",
|
||||
"voice": "Stimme",
|
||||
"voicePlaceholder": "alloy",
|
||||
"language": "Sprache",
|
||||
"languagePlaceholder": "de",
|
||||
"instructions": "Anweisungen",
|
||||
"instructionsPlaceholder": "Persönlichkeit des Assistenten festlegen..."
|
||||
},
|
||||
"status": {
|
||||
"idle": "Inaktiv",
|
||||
"connecting": "Verbinden...",
|
||||
"listening": "Höre zu...",
|
||||
"speaking": "Spricht...",
|
||||
"ended": "Sitzung beendet"
|
||||
},
|
||||
"toasts": {
|
||||
"noModel": "Wählen Sie zuerst ein Modell",
|
||||
"connectFailed": "Verbindung fehlgeschlagen: {{message}}"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"title": "Verlauf",
|
||||
"empty": "Noch kein Verlauf",
|
||||
"deleteEntry": "Eintrag löschen",
|
||||
"clear": "Verlauf löschen",
|
||||
"clearTitle": "Gesamten Verlauf löschen",
|
||||
"clearMessage": "Alle Verlaufseinträge entfernen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"clearConfirm": "Löschen",
|
||||
"cleared": "Verlauf gelöscht"
|
||||
},
|
||||
"request": {
|
||||
"heading": "Request",
|
||||
"copyCurl": "Copy as curl",
|
||||
"copied": "Copied"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,863 @@
|
||||
{
|
||||
"studio": {
|
||||
"tabs": {
|
||||
"images": "Images",
|
||||
"video": "Video",
|
||||
"tts": "TTS",
|
||||
"sound": "Sound",
|
||||
"transform": "Transform",
|
||||
"threed": "3D",
|
||||
"overview": "Overview",
|
||||
"diarization": "Diarization"
|
||||
},
|
||||
"tabState": {
|
||||
"on": "(model installed)",
|
||||
"off": "(no model installed)"
|
||||
},
|
||||
"overview": {
|
||||
"title": "Studio",
|
||||
"subtitle": "Images, video, 3D, speech and sound, made with the models on this machine."
|
||||
},
|
||||
"groups": {
|
||||
"create": "Create",
|
||||
"voice": "Voice",
|
||||
"transform": "Transform"
|
||||
},
|
||||
"composer": {
|
||||
"title": "What do you want to make?",
|
||||
"lede": "Describe it and Studio suggests the type. Alt+1 to 7 picks one.",
|
||||
"startTyping": "start typing",
|
||||
"prompt": "Prompt",
|
||||
"placeholder": {
|
||||
"images": "Describe the image you want…",
|
||||
"video": "Describe the shot: subject, camera move, light…",
|
||||
"tts": "Type the words to speak…",
|
||||
"sound": "Describe the sound or music…"
|
||||
},
|
||||
"fileLead": {
|
||||
"threed": "3D turns one picture into an object. Start from one of your pictures below, or add a file on the next page.",
|
||||
"transform": "Transform cleans up a recording or changes a voice. Start from one of your recordings below, or add a file on the next page.",
|
||||
"diarization": "Diarization finds who spoke and when in a recording. Start from one of your recordings below, or add a file on the next page."
|
||||
},
|
||||
"starters": "Starters",
|
||||
"soundsLike": "Sounds like {{type}}.",
|
||||
"soundsLikeMissing": "Sounds like {{type}}, and no {{type}} model is installed yet.",
|
||||
"switchTo": "Switch to {{type}}",
|
||||
"seeNeeds": "See what it needs",
|
||||
"model": "Model",
|
||||
"noModel": "No model installed",
|
||||
"size": "Size",
|
||||
"count": "How many",
|
||||
"countOption_one": "{{count}} image",
|
||||
"countOption_other": "{{count}} images",
|
||||
"startFrom": "Start from",
|
||||
"sourceNone": "Start from nothing",
|
||||
"sourceOnPage": "Add a file on the next page",
|
||||
"fit": {
|
||||
"fits": "Needs {{need}}, fits with {{amount}} GB to spare",
|
||||
"spill": "Needs {{need}}, {{amount}} GB spills to RAM",
|
||||
"over": "Needs {{need}}, {{amount}} GB more than this machine has"
|
||||
},
|
||||
"whyModel": "Install the {{type}} model first",
|
||||
"whyPrompt": "Write a prompt first",
|
||||
"generate": "Generate",
|
||||
"openType": "Open {{type}}",
|
||||
"firstRun": "Nothing is installed yet. Install the one you want first. You can add the others later, or never."
|
||||
},
|
||||
"starters": {
|
||||
"images": [
|
||||
{
|
||||
"label": "Harbour at first light",
|
||||
"text": "A fishing harbour at first light, pastel houses, long reflections, 35 mm film grain"
|
||||
},
|
||||
{
|
||||
"label": "Paper-cut mountains",
|
||||
"text": "Paper-cut mountains in layered teal and amber, soft haze"
|
||||
},
|
||||
{
|
||||
"label": "Macro fern",
|
||||
"text": "Macro photo of a dew-covered fern, shallow depth of field"
|
||||
}
|
||||
],
|
||||
"video": [
|
||||
{
|
||||
"label": "Misty forest dolly",
|
||||
"text": "Slow dolly through a misty pine forest at sunrise"
|
||||
},
|
||||
{
|
||||
"label": "Waves on black sand",
|
||||
"text": "Waves rolling onto black sand, drone shot"
|
||||
}
|
||||
],
|
||||
"tts": [
|
||||
{
|
||||
"label": "Welcome line",
|
||||
"text": "Welcome to the harbour tour. The first boats leave at six, and the bakery opens at five."
|
||||
},
|
||||
{
|
||||
"label": "Release note",
|
||||
"text": "Release 3.2 is out. It adds a faster scheduler and fixes the model unload bug."
|
||||
}
|
||||
],
|
||||
"sound": [
|
||||
{
|
||||
"label": "Lo-fi piano",
|
||||
"text": "Lo-fi piano with rain on glass"
|
||||
},
|
||||
{
|
||||
"label": "Wind in pines",
|
||||
"text": "Wind through pines, no music"
|
||||
}
|
||||
],
|
||||
"threed": [],
|
||||
"transform": [],
|
||||
"diarization": []
|
||||
},
|
||||
"install": {
|
||||
"title": "No {{type}} model installed",
|
||||
"size": "is {{size}} to download.",
|
||||
"sizeUnknown": "has no download size listed.",
|
||||
"need": "It needs about {{need}} of memory while it runs.",
|
||||
"needUnknown": "Its memory need is not listed.",
|
||||
"free": "{{free}} free now.",
|
||||
"nothingStarts": "Nothing starts until you install it.",
|
||||
"button": "Install {{model}}",
|
||||
"installing": "Installing {{model}}",
|
||||
"started": "Installing {{model}}",
|
||||
"failed": "The install did not start: {{message}}",
|
||||
"galleryDown": "The model gallery could not be reached, so there is nothing to suggest. Check the connection, or add a model by hand.",
|
||||
"noneListed": "The gallery lists no {{type}} model.",
|
||||
"browse": "Browse all models"
|
||||
},
|
||||
"work": {
|
||||
"title": "Your work",
|
||||
"results_one": "{{count}} result",
|
||||
"results_other": "{{count}} results",
|
||||
"nResults_one": "{{count}} result",
|
||||
"nResults_other": "{{count}} results",
|
||||
"group": "Group related results",
|
||||
"clear": "Clear history",
|
||||
"clearTitle": "Clear your work history?",
|
||||
"clearBody": "This removes the list of results and favourites from this browser. Files already made stay on the server.",
|
||||
"clearConfirm": "Clear history",
|
||||
"filters": "Filter your work",
|
||||
"all": "All",
|
||||
"favourites": "Favourites",
|
||||
"favourite": "Favourite",
|
||||
"open": "Open {{title}}",
|
||||
"openProject": "Open {{title}}, {{count}} results that came from each other",
|
||||
"project": "Project",
|
||||
"emptyTitle": "Nothing here yet",
|
||||
"emptyBody": "What you make shows up here, so you can find it again and build on it.",
|
||||
"emptyFilterTitle": "No {{filter}} yet",
|
||||
"emptyFilterBody": "Pick another filter, or make something.",
|
||||
"fileGone": "The file is no longer on the server",
|
||||
"untitled": {
|
||||
"images": "Picture",
|
||||
"video": "Clip",
|
||||
"threed": "3D object",
|
||||
"tts": "Speech",
|
||||
"sound": "Sound",
|
||||
"transform": "Transformed audio",
|
||||
"diarization": "Recording"
|
||||
}
|
||||
},
|
||||
"lineage": {
|
||||
"back": "Your work",
|
||||
"sub_one": "{{count}} result",
|
||||
"sub_other": "{{count}} results that came from each other",
|
||||
"subOne": "One result",
|
||||
"board": "How these results were made, from the prompt on the left",
|
||||
"prompt": "Prompt",
|
||||
"file": "File",
|
||||
"suggested": "Suggested",
|
||||
"draft": "Draft",
|
||||
"needsModel": "Needs a {{type}} model",
|
||||
"ready": "Ready",
|
||||
"dock": "Selected result",
|
||||
"details": "Show details",
|
||||
"take": "Run as a new take",
|
||||
"branch": "Branch from here",
|
||||
"cancel": "Cancel",
|
||||
"openIn": "Open in {{type}}",
|
||||
"newBranch": "New branch: {{step}}",
|
||||
"fromResult": "from {{title}}",
|
||||
"makeFrom": "Make from this result",
|
||||
"targets": "What to make next",
|
||||
"describe": "Describe it",
|
||||
"keeps": "Keeps the original. Opens the workspace with this filled in; nothing runs until you press Generate there.",
|
||||
"promptLabel": "Prompt",
|
||||
"continue": "Continue from this result",
|
||||
"download": "Download",
|
||||
"took": "took {{seconds}} s",
|
||||
"speakers_one": "{{count}} speaker",
|
||||
"speakers_other": "{{count}} speakers",
|
||||
"noInput": "The original input was not kept, so this cannot be run again as it was.",
|
||||
"noBranch": "Nothing accepts a {{type}} as a starting point yet.",
|
||||
"cannotStart": "The {{page}} page cannot start from a {{what}} yet.",
|
||||
"goneTitle": "That result is no longer in this browser",
|
||||
"goneBody": "It may have been cleared from the history. Go back to your work to see what is left."
|
||||
},
|
||||
"edges": {
|
||||
"take": "take",
|
||||
"animate": "animate",
|
||||
"to-3d": "to 3D",
|
||||
"variation": "variation",
|
||||
"transform": "transform",
|
||||
"diarize": "who spoke",
|
||||
"soundtrack": "soundtrack",
|
||||
"turntable": "turntable",
|
||||
"upscale": "upscale"
|
||||
},
|
||||
"steps": {
|
||||
"animate": {
|
||||
"title": "Animate",
|
||||
"why": "Turn this picture into a short clip",
|
||||
"template": "Slow push-in, small natural motion"
|
||||
},
|
||||
"to-3d": {
|
||||
"title": "Make it 3D",
|
||||
"why": "Build an object you can turn around",
|
||||
"plain": "3D starts from this picture. You choose the model and options on the next page."
|
||||
},
|
||||
"variation": {
|
||||
"title": "Variation",
|
||||
"why": "Another take with edits"
|
||||
},
|
||||
"transform": {
|
||||
"title": "Transform",
|
||||
"why": "Clean up this audio or change the voice",
|
||||
"plain": "Transform works on this audio. You choose the model and options on the next page."
|
||||
},
|
||||
"diarize": {
|
||||
"title": "Find who spoke",
|
||||
"why": "Mark each speaker on a timeline",
|
||||
"plain": "This opens the recording in Diarization. You choose the model on the next page."
|
||||
},
|
||||
"soundtrack": {
|
||||
"title": "Add a soundtrack",
|
||||
"why": "Sound that fits the shot"
|
||||
},
|
||||
"turntable": {
|
||||
"title": "Turntable clip",
|
||||
"why": "Show the object from every side"
|
||||
},
|
||||
"upscale": {
|
||||
"title": "Upscale",
|
||||
"why": "Increase the resolution of this image."
|
||||
}
|
||||
},
|
||||
"kinds": {
|
||||
"video": "video",
|
||||
"mesh": "3D object"
|
||||
},
|
||||
"handoff": {
|
||||
"aResult": "a result from Studio",
|
||||
"loading": "Loading {{name}} as the starting point…",
|
||||
"ready": "Starting from {{name}}.",
|
||||
"missing": "The result this was made from is no longer in this browser's history, so there is no starting point. You can still add a file below.",
|
||||
"error": "{{name}} could not be loaded. The server may have cleaned up the file. You can add a file below.",
|
||||
"remove": "Remove"
|
||||
},
|
||||
"error": {
|
||||
"title": "Studio could not read the installed models",
|
||||
"body": "The list of models did not load, so Studio cannot say which types are ready. Your work below is not affected.",
|
||||
"retry": "Try again"
|
||||
},
|
||||
"workspace": {
|
||||
"sourcesLabel": "Starting points",
|
||||
"lede": {
|
||||
"images": "Text to image, image to image, reference images",
|
||||
"video": "Text to video and image to video",
|
||||
"threed": "One picture in, one 3D object out",
|
||||
"tts": "Text to speech, with a saved voice when the model clones",
|
||||
"sound": "Music and sound from a description",
|
||||
"transform": "Clean up a recording, or change a voice",
|
||||
"diarization": "Find who spoke and when"
|
||||
},
|
||||
"sources": {
|
||||
"startImage": "Start from an image",
|
||||
"endImage": "End image",
|
||||
"chosen": "Chosen file",
|
||||
"audio": "Audio",
|
||||
"reference": "Reference"
|
||||
},
|
||||
"fields": {
|
||||
"prompt": "text",
|
||||
"model": "model",
|
||||
"size": "size",
|
||||
"count": "count",
|
||||
"steps": "steps",
|
||||
"seed": "seed",
|
||||
"negative": "negative prompt",
|
||||
"duration": "duration",
|
||||
"fps": "fps",
|
||||
"frames": "frames",
|
||||
"cfg": "CFG",
|
||||
"quality": "quality",
|
||||
"background": "background",
|
||||
"voice": "voice",
|
||||
"instructions": "delivery",
|
||||
"mode": "mode",
|
||||
"caption": "caption",
|
||||
"lyrics": "lyrics",
|
||||
"bpm": "BPM",
|
||||
"key": "key",
|
||||
"language": "language",
|
||||
"timesignature": "time signature",
|
||||
"instrumental": "instrumental",
|
||||
"think": "think mode",
|
||||
"params": "parameters",
|
||||
"speakers": "speakers",
|
||||
"detail": "detail"
|
||||
},
|
||||
"changedFrom": "Changed from this take:",
|
||||
"edited": "edited",
|
||||
"runAgain": "Run again",
|
||||
"useIn": "Use in",
|
||||
"rerun": "Re-run with edits",
|
||||
"lineage": "Lineage",
|
||||
"download": "Download",
|
||||
"tools": "Result actions",
|
||||
"from": "from",
|
||||
"fromGone": "from a result that is no longer in this browser ({{edge}})",
|
||||
"failed": {
|
||||
"title": "That did not finish",
|
||||
"note": "Your prompt and settings are still here.",
|
||||
"retry": "Try again",
|
||||
"traces": "View traces for details"
|
||||
},
|
||||
"strip": {
|
||||
"title": "Recent results",
|
||||
"allWork": "All work",
|
||||
"noFavourites": "No favourites yet. Star a result to keep it here."
|
||||
},
|
||||
"auto": "Auto",
|
||||
"sound": {
|
||||
"more": "More options",
|
||||
"generated": "Sound generated",
|
||||
"noCaption": "Please enter caption or lyrics"
|
||||
},
|
||||
"transform": {
|
||||
"noModel": "Please select a model",
|
||||
"noAudio": "Please choose an audio file",
|
||||
"done": "Audio transformed",
|
||||
"inputGone": "The audio that run used is no longer on the server. Add it again to run it.",
|
||||
"loadReference": "Load a reference first",
|
||||
"noMic": "This browser does not expose the microphone",
|
||||
"recorded": "Recorded the microphone with the reference echo. Press Transform to test echo cancellation.",
|
||||
"echoFailed": "Echo test failed: {{message}}",
|
||||
"recording": "recording {{seconds}} s",
|
||||
"whyAudio": "Add an audio file first",
|
||||
"waiting": "Your audio, waiting to be transformed",
|
||||
"paramCount_one": "{{count}} parameter",
|
||||
"paramCount_other": "{{count}} parameters"
|
||||
},
|
||||
"threed": {
|
||||
"whyImage": "Add a picture first"
|
||||
},
|
||||
"choose": "Choose",
|
||||
"replace": "Replace",
|
||||
"dropHint": "or drop it here",
|
||||
"diarization": {
|
||||
"whyFile": "Choose a recording first",
|
||||
"hint": "Audio or video, or drop it here",
|
||||
"timeline": "Timeline of {{count}} speakers over {{length}}",
|
||||
"segmentCount_one": "{{count}} segment",
|
||||
"segmentCount_other": "{{count}} segments",
|
||||
"talk": "{{seconds}} s of talk, {{percent}}%",
|
||||
"notKept": "The recording and the transcript are not kept, so only the summary of this run is shown. Run it again with the file to see the timeline.",
|
||||
"empty": "Choose a recording to see who spoke and when"
|
||||
}
|
||||
},
|
||||
"upscale": {
|
||||
"model": "Upscaling model",
|
||||
"scale": "Scale",
|
||||
"source": "Source image",
|
||||
"loading": "Upscaling…",
|
||||
"failed": "Upscale failed: {{message}}",
|
||||
"noResult": "No output image was returned."
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"title": "Image Generation",
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Describe the image you want to generate...",
|
||||
"negativePrompt": "Negative Prompt",
|
||||
"negativePromptPlaceholder": "What to avoid...",
|
||||
"size": "Size",
|
||||
"count": "Count (1-4)",
|
||||
"advanced": "Advanced Settings",
|
||||
"imageInputs": "Image Inputs",
|
||||
"steps": "Steps",
|
||||
"stepsPlaceholder": "20",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Random",
|
||||
"sourceImage": "Source Image (img2img)",
|
||||
"refImages": "Reference Images",
|
||||
"refImagesAdded_one": "{{count}} image added",
|
||||
"refImagesAdded_other": "{{count}} images added"
|
||||
},
|
||||
"actions": {
|
||||
"view": "View",
|
||||
"generate": "Generate",
|
||||
"generating": "Generating..."
|
||||
},
|
||||
"empty": "Generated images will appear here",
|
||||
"toasts": {
|
||||
"noPrompt": "Please enter a prompt",
|
||||
"noModel": "Please select a model",
|
||||
"noResults": "No images generated"
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"title": "Video Generation",
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Describe the video you want to generate...",
|
||||
"duration": "Duration (seconds)",
|
||||
"fps": "FPS",
|
||||
"size": "Size",
|
||||
"advanced": "Advanced Settings",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Random",
|
||||
"frames": "Frames",
|
||||
"referenceMedia": "Reference media",
|
||||
"startImage": "Start image",
|
||||
"endImage": "End image",
|
||||
"avatarAudio": "Avatar audio"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generate",
|
||||
"generating": "Generating..."
|
||||
},
|
||||
"empty": "Generated video will appear here",
|
||||
"toasts": {
|
||||
"noPrompt": "Please enter a prompt",
|
||||
"noModel": "Please select a model",
|
||||
"noResults": "No video generated"
|
||||
}
|
||||
},
|
||||
"threed": {
|
||||
"title": "3D Generation",
|
||||
"animation": {
|
||||
"skeletonHint": "Generates an animated skeleton, without a mesh or skin.",
|
||||
"preview": "Animated skeleton preview",
|
||||
"pause": "Pause",
|
||||
"play": "Play",
|
||||
"reset": "Reset view",
|
||||
"timeline": "Timeline",
|
||||
"zoom": "Zoom",
|
||||
"inputTooLong": "Input exceeds the {{limit}} byte limit.",
|
||||
"fileTooLarge": "The selected file is too large.",
|
||||
"readFailed": "Could not read the selected file."
|
||||
},
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"image": "Input image",
|
||||
"quality": "Quality",
|
||||
"quality_auto": "Auto (best available)",
|
||||
"quality_coarse": "Coarse preview (fast)",
|
||||
"quality_512": "512³ fine",
|
||||
"quality_1024": "1024³ cascade (slow)",
|
||||
"background": "Background",
|
||||
"background_auto": "Auto-remove solid background",
|
||||
"background_keep": "Keep original",
|
||||
"background_black": "Remove black",
|
||||
"background_white": "Remove white",
|
||||
"advanced": "Advanced Settings",
|
||||
"steps": "Shape steps",
|
||||
"textureSteps": "Material steps",
|
||||
"guidance": "Guidance",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Random"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generate",
|
||||
"generating": "Generating...",
|
||||
"remesh": "Apply remeshing",
|
||||
"remeshing": "Applying remeshing...",
|
||||
"showOriginal": "Show original",
|
||||
"download": "Download GLB"
|
||||
},
|
||||
"remesh": {
|
||||
"title": "Watertight print remesh",
|
||||
"detail": "Remesh detail",
|
||||
"coarser": "Coarser · faster",
|
||||
"finer": "Finer · slower",
|
||||
"hint": "Detail controls the smallest preserved features. The enclosing offset follows it automatically.",
|
||||
"ready": "Previewing the watertight remeshed model. This is the version that will be downloaded."
|
||||
},
|
||||
"viewer": {
|
||||
"wireframe": "Wireframe",
|
||||
"autoRotate": "Auto-rotate",
|
||||
"noWebgl": "WebGL2 is not available in this browser — download the GLB to view it elsewhere.",
|
||||
"contextLost": "The 3D view lost the GPU context — reload the page to restore it.",
|
||||
"stats": "{{verts}} vertices · {{tris}} triangles",
|
||||
"hint": "drag: rotate · pinch/wheel: zoom · two-finger/right-drag: pan · double-click: reset"
|
||||
},
|
||||
"empty": "Generated 3D model will appear here",
|
||||
"toasts": {
|
||||
"noImage": "Please provide an input image",
|
||||
"noModel": "Please select a model",
|
||||
"noResults": "No model generated"
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"title": "Text to Speech",
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"voice": "Voice",
|
||||
"voicePlaceholder": "Optional speaker or voice ID",
|
||||
"input": "Text",
|
||||
"inputPlaceholder": "Enter text to synthesize...",
|
||||
"instructions": "Instructions",
|
||||
"instructionsPlaceholder": "For example: Speak slowly and warmly",
|
||||
"instructionsHint": "Optional delivery guidance. Support depends on the backend."
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generate",
|
||||
"generating": "Generating..."
|
||||
},
|
||||
"empty": "Generated audio will appear here",
|
||||
"toasts": {
|
||||
"noText": "Please enter text",
|
||||
"noModel": "Please select a model",
|
||||
"generated": "Speech generated",
|
||||
"generateFailed": "Generation failed"
|
||||
},
|
||||
"voiceLibrary": {
|
||||
"cloningReady": "Voice cloning",
|
||||
"loading": "Loading saved personalities…",
|
||||
"modelDefault": "Use model default",
|
||||
"empty": "No saved personalities yet.",
|
||||
"create": "Create one",
|
||||
"manage": "Manage Personality Library",
|
||||
"namedVoiceHint": "This model does not accept reference-audio profiles. You can still enter a named speaker supported by its backend."
|
||||
}
|
||||
},
|
||||
"voiceLibrary": {
|
||||
"title": "Speech voices",
|
||||
"subtitle": "A recording and its exact transcript. Text-to-speech models that accept reference audio can speak with it.",
|
||||
"loadFailed": "Speech voices could not be loaded",
|
||||
"loading": "Loading saved personalities…",
|
||||
"listLabel": "Saved voice personalities",
|
||||
"status": {
|
||||
"ready": "Ready"
|
||||
},
|
||||
"summary": {
|
||||
"label": "Voice library status",
|
||||
"profiles": "saved personalities",
|
||||
"modelsReady_one": "{{count}} compatible model ready",
|
||||
"modelsReady_other": "{{count}} compatible models ready",
|
||||
"noModels": "No compatible model installed"
|
||||
},
|
||||
"search": {
|
||||
"label": "Search voice profiles",
|
||||
"placeholder": "Search voices, languages, or transcripts"
|
||||
},
|
||||
"filters": {
|
||||
"language": "Filter by language",
|
||||
"allLanguages": "All languages"
|
||||
},
|
||||
"empty": {
|
||||
"title": "No speech voices yet",
|
||||
"body": "Record or upload a consented reference clip, add its exact transcript, and reuse the voice across compatible text-to-speech models."
|
||||
},
|
||||
"noResults": {
|
||||
"title": "No voices match these filters",
|
||||
"body": "Try a different search or show every language."
|
||||
},
|
||||
"detail": {
|
||||
"eyebrow": "Reference recording and exact transcript",
|
||||
"close": "Close",
|
||||
"emptyTitle": "Select a voice to inspect it",
|
||||
"emptyBody": "Reference audio, transcript, compatibility, and consent details will appear here.",
|
||||
"referenceAudio": "Reference audio",
|
||||
"transcript": "Reference transcript"
|
||||
},
|
||||
"metadata": {
|
||||
"language": "Language",
|
||||
"languageUnknown": "Not specified",
|
||||
"duration": "Duration",
|
||||
"sampleRate": "Sample rate",
|
||||
"created": "Created"
|
||||
},
|
||||
"consent": {
|
||||
"label": "Permission",
|
||||
"confirmed": "Consent confirmed",
|
||||
"confirmedAt": "Confirmed {{date}}"
|
||||
},
|
||||
"modelSetup": {
|
||||
"title": "Install a voice-cloning model",
|
||||
"body": "This voice is ready, but it needs a model that accepts reference audio. Choose one verified by this server.",
|
||||
"loading": "Finding compatible models in your galleries…",
|
||||
"backendUnknown": "Backend selected during install",
|
||||
"install": "Install",
|
||||
"installing": "Installing…",
|
||||
"loadFailed": "Compatible recommendations could not be loaded.",
|
||||
"noneAvailable": "No compatible uninstalled model is available in the configured galleries.",
|
||||
"capabilityNote": "Recommendations come from model capability metadata, not a browser-side model list."
|
||||
},
|
||||
"api": {
|
||||
"title": "API usage and compatible models",
|
||||
"summary": "Use this voice outside the browser or check model support",
|
||||
"body": "Send the profile’s stable voice URI with each speech request. LocalAI resolves the private reference audio and transcript at generation time; model YAML is not modified.",
|
||||
"compatibleModels": "Compatible installed models",
|
||||
"noInstalledModels": "None yet. Install one of the server-verified models above before sending this request.",
|
||||
"curlExample": "cURL example",
|
||||
"copy": "Copy request",
|
||||
"endpointNote": "The same model, input, and voice fields also work with POST /tts."
|
||||
},
|
||||
"actions": {
|
||||
"create": "Add a speech voice",
|
||||
"createFirst": "Add your first speech voice",
|
||||
"retry": "Try again",
|
||||
"clearFilters": "Clear filters",
|
||||
"useInTTS": "Use in Text to Speech",
|
||||
"delete": "Delete voice",
|
||||
"installModel": "Install a compatible model",
|
||||
"browseModels": "Browse all models"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Delete this voice?",
|
||||
"message": "“{{name}}” and its reference audio will be permanently removed. Existing model YAML is not changed.",
|
||||
"deleting": "Deleting…"
|
||||
},
|
||||
"toasts": {
|
||||
"deleted": "Deleted {{name}}",
|
||||
"installStarted": "Installing {{name}}. Progress is available in Operations.",
|
||||
"installFailed": "Could not start installation: {{message}}",
|
||||
"apiCopied": "API request copied",
|
||||
"copyFailed": "Could not copy the API request"
|
||||
}
|
||||
},
|
||||
"voiceCreate": {
|
||||
"title": "Add a speech voice",
|
||||
"subtitle": "A speech voice is one or more clean reference clips and their exact transcripts. Text-to-speech models that accept reference audio can speak with it.",
|
||||
"references": {
|
||||
"add": "Add another reference",
|
||||
"additional": "Reference {{number}}",
|
||||
"remove": "Remove"
|
||||
},
|
||||
"sections": {
|
||||
"reference": {
|
||||
"title": "Reference audio",
|
||||
"body": "Record in the browser or upload an existing clip. Speech should be clean, natural, and from one speaker."
|
||||
},
|
||||
"details": {
|
||||
"title": "Identity and transcript",
|
||||
"body": "Give admins a recognizable name and transcribe the clip exactly, including fillers and punctuation."
|
||||
},
|
||||
"consent": {
|
||||
"title": "Authorization",
|
||||
"body": "Voice cloning can be sensitive. Confirm permission before this reference is saved."
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"label": "Voice reference",
|
||||
"normalizing": "Preparing a secure PCM WAV…",
|
||||
"preview": "Normalized reference",
|
||||
"tooLarge": "This file is larger than 50 MiB.",
|
||||
"durationError": "Reference audio must be between 1 second and 2 minutes.",
|
||||
"decodeError": "This browser could not decode the selected audio file.",
|
||||
"qualityReady": "Recommended length",
|
||||
"qualityHint": "6–30 seconds usually produces a stronger clone"
|
||||
},
|
||||
"fields": {
|
||||
"name": "Voice name",
|
||||
"namePlaceholder": "e.g. Documentary narrator",
|
||||
"language": "Language (optional)",
|
||||
"languagePlaceholder": "e.g. en-US",
|
||||
"description": "Description (optional)",
|
||||
"descriptionPlaceholder": "Tone, source, or intended use",
|
||||
"transcript": "Exact transcript",
|
||||
"transcriptPlaceholder": "Type every word spoken in the reference clip…",
|
||||
"transcriptHint": "Match the recording exactly. Do not rewrite grammar or remove pauses and fillers."
|
||||
},
|
||||
"consent": {
|
||||
"title": "I confirm that this voice may be cloned",
|
||||
"body": "I have the speaker’s permission or another lawful basis to store and use this recording for synthesized speech."
|
||||
},
|
||||
"readiness": {
|
||||
"title": "Profile readiness",
|
||||
"body": "A voice becomes available in Text to Speech when every required item is complete.",
|
||||
"audio": "Valid PCM-WAV reference",
|
||||
"quality": "6–30 second quality window",
|
||||
"name": "Recognizable voice name",
|
||||
"transcript": "Exact reference transcript",
|
||||
"consent": "Authorization confirmed"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Private by default. ",
|
||||
"body": "Reference audio is stored in LocalAI’s data directory with restricted file permissions and is never exposed as a model-folder path."
|
||||
},
|
||||
"actions": {
|
||||
"back": "Back to library",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save voice",
|
||||
"saving": "Saving…"
|
||||
},
|
||||
"toasts": {
|
||||
"created": "{{name}} is ready to use"
|
||||
}
|
||||
},
|
||||
"sound": {
|
||||
"title": "Sound Generation",
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Describe the sound you want to generate...",
|
||||
"instrumental": "Instrumental",
|
||||
"bpm": "BPM",
|
||||
"duration": "Duration (seconds)",
|
||||
"language": "Language",
|
||||
"timesignature": "Time Signature",
|
||||
"keyscale": "Key/Scale",
|
||||
"vocalLanguage": "Vocal language",
|
||||
"vocalLanguagePlaceholder": "e.g. English",
|
||||
"caption": "Caption",
|
||||
"lyrics": "Lyrics",
|
||||
"lyricsPlaceholder": "Lyrics for vocal generation",
|
||||
"simple": "Simple",
|
||||
"advanced": "Advanced",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Random",
|
||||
"thinkMode": "Think mode"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generate",
|
||||
"generating": "Generating..."
|
||||
},
|
||||
"empty": "Generated audio will appear here",
|
||||
"toasts": {
|
||||
"noPrompt": "Please enter a prompt",
|
||||
"noModel": "Please select a model",
|
||||
"generateFailed": "Generation failed"
|
||||
},
|
||||
"expansion": {
|
||||
"label": "Prompt expansion model",
|
||||
"placeholder": "Choose a model or backend default",
|
||||
"backendDefault": "Backend default",
|
||||
"choose": "Choose a chat model or Backend default to continue.",
|
||||
"loading": "Loading installed chat models…",
|
||||
"error": "Cannot load chat models. Choose Backend default or return to this tab to retry.",
|
||||
"noChat": "No installed chat models. Choose Backend default to use the audio backend.",
|
||||
"help": "A chat model expands your description into style and lyrics. Backend default uses the audio backend’s existing prompt behavior."
|
||||
}
|
||||
},
|
||||
"audioTransform": {
|
||||
"title": "Audio Transform",
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"audio": "Audio (required)",
|
||||
"reference": "Reference (optional)",
|
||||
"referenceHelp": "Loopback / far-end signal for echo cancellation, target speaker for voice conversion. Leave empty for unconditional transform.",
|
||||
"advancedParameters": "Advanced parameters",
|
||||
"advancedParametersHelp": "backend-specific (one key=value per line, e.g. noise_gate=true)",
|
||||
"advancedParametersPlaceholder": "# Optional. For LocalVQE:\n# noise_gate=true\n# noise_gate_threshold_dbfs=-50"
|
||||
},
|
||||
"input": {
|
||||
"upload": "Upload",
|
||||
"uploadDescription": "Drop a file here or",
|
||||
"uploadBrowse": "browse",
|
||||
"record": "Record",
|
||||
"startRecording": "Start recording",
|
||||
"stop": "Stop",
|
||||
"encoding": "Encoding...",
|
||||
"clear": "Clear",
|
||||
"microphoneUnavailable": "Microphone capture is unavailable in this browser.",
|
||||
"echoNotice": "Browsers often apply their own WebRTC echo cancellation and noise suppression by default. This usually results in worse performance than running LocalVQE on the raw audio.",
|
||||
"echoTest": "Echo test (record mic while playing reference)",
|
||||
"stopEchoTest": "Stop echo test"
|
||||
},
|
||||
"result": {
|
||||
"audio": "Audio",
|
||||
"reference": "Reference",
|
||||
"inputSpectrum": "Input spectrum",
|
||||
"outputSpectrum": "Output spectrum",
|
||||
"outputSpectrumHint": "Transform to compare attenuation",
|
||||
"output": "Output"
|
||||
},
|
||||
"actions": {
|
||||
"transform": "Transform",
|
||||
"processing": "Processing..."
|
||||
},
|
||||
"empty": "Choose an audio file (and optional reference) to transform"
|
||||
},
|
||||
"talk": {
|
||||
"title": "Talk",
|
||||
"subtitle": "Realtime voice conversation",
|
||||
"actions": {
|
||||
"start": "Start session",
|
||||
"stop": "End session",
|
||||
"connecting": "Connecting...",
|
||||
"muted": "Muted",
|
||||
"mute": "Mute",
|
||||
"unmute": "Unmute"
|
||||
},
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"voice": "Voice",
|
||||
"voicePlaceholder": "alloy",
|
||||
"language": "Language",
|
||||
"languagePlaceholder": "en",
|
||||
"instructions": "Instructions",
|
||||
"instructionsPlaceholder": "Set the assistant's persona..."
|
||||
},
|
||||
"status": {
|
||||
"idle": "Idle",
|
||||
"connecting": "Connecting...",
|
||||
"listening": "Listening...",
|
||||
"speaking": "Speaking...",
|
||||
"ended": "Session ended"
|
||||
},
|
||||
"toasts": {
|
||||
"noModel": "Select a model first",
|
||||
"connectFailed": "Failed to connect: {{message}}"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"title": "History",
|
||||
"empty": "No history yet",
|
||||
"deleteEntry": "Delete entry",
|
||||
"clear": "Clear history",
|
||||
"clearTitle": "Clear all history",
|
||||
"clearMessage": "Remove all history entries? This cannot be undone.",
|
||||
"clearConfirm": "Clear",
|
||||
"cleared": "History cleared"
|
||||
},
|
||||
"request": {
|
||||
"heading": "Request",
|
||||
"copyCurl": "Copy as curl",
|
||||
"copied": "Copied"
|
||||
},
|
||||
"diarization": {
|
||||
"title": "Speaker diarization",
|
||||
"subtitle": "Upload a recording to see who spoke when. Preview clean speech before remembering a voice.",
|
||||
"model": "Model",
|
||||
"recording": "Recording",
|
||||
"optIn": "Prepare speakers to remember",
|
||||
"warning": "Remembered voices are shared globally on this server and are lost when it restarts. Nothing is remembered automatically.",
|
||||
"manage": "Manage remembered voices",
|
||||
"running": "Diarizing…",
|
||||
"run": "Diarize",
|
||||
"unsupported": "Choose a profile-capable model with a speaker encoder, or turn off “Prepare speakers to remember” to run normal diarization.",
|
||||
"previewError": "Could not play this recording. Check that your browser supports its audio format.",
|
||||
"speakers": "Speakers",
|
||||
"segments": "Segments",
|
||||
"duration": "Clean speech: {{seconds}} seconds",
|
||||
"preview": "Preview {{number}}",
|
||||
"insufficient": "Not enough clean speech or no usable profile. Try a recording with longer, non-overlapping speech.",
|
||||
"nameAndRemember": "Name and remember",
|
||||
"name": "Name",
|
||||
"saving": "Saving…",
|
||||
"remember": "Remember",
|
||||
"cancel": "Cancel",
|
||||
"stop": "Stop preview",
|
||||
"missingProfiles": "The backend did not return the requested speaker profiles."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"studio": {
|
||||
"tabs": {
|
||||
"images": "Imágenes",
|
||||
"video": "Video",
|
||||
"tts": "TTS",
|
||||
"sound": "Sonido",
|
||||
"transform": "Transform",
|
||||
"overview": "Overview"
|
||||
},
|
||||
"overview": {
|
||||
"eyebrow": "{{ready}} of {{total}} modalities ready",
|
||||
"title": "Studio",
|
||||
"subtitle": "Generate images, video, 3D, speech and sound with the models on this machine.",
|
||||
"canMake": "What you can make",
|
||||
"running": "Running now",
|
||||
"recent": "Recent outputs",
|
||||
"noModel": "No model installed",
|
||||
"install": "Install a model",
|
||||
"ready": "Ready",
|
||||
"seconds": "{{seconds}}s",
|
||||
"describe": {
|
||||
"images": "Text to image, image to image, reference images",
|
||||
"video": "Text to video and image to video",
|
||||
"threed": "Image to mesh reconstruction",
|
||||
"tts": "Text to speech using your voice library",
|
||||
"sound": "Music and sound effects from a prompt",
|
||||
"transform": "Separation, enhancement and voice conversion"
|
||||
}
|
||||
},
|
||||
"groups": {
|
||||
"create": "Create",
|
||||
"voice": "Voice",
|
||||
"transform": "Transform"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"title": "Generación de imágenes",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Describe la imagen que quieres generar...",
|
||||
"negativePrompt": "Prompt negativo",
|
||||
"negativePromptPlaceholder": "Qué evitar...",
|
||||
"size": "Tamaño",
|
||||
"count": "Cantidad (1-4)",
|
||||
"advanced": "Configuración avanzada",
|
||||
"imageInputs": "Entradas de imagen",
|
||||
"steps": "Pasos",
|
||||
"stepsPlaceholder": "20",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Aleatorio",
|
||||
"sourceImage": "Imagen fuente (img2img)",
|
||||
"refImages": "Imágenes de referencia",
|
||||
"refImagesAdded_one": "{{count}} imagen añadida",
|
||||
"refImagesAdded_other": "{{count}} imágenes añadidas"
|
||||
},
|
||||
"actions": {
|
||||
"view": "View",
|
||||
"generate": "Generar",
|
||||
"generating": "Generando..."
|
||||
},
|
||||
"empty": "Las imágenes generadas aparecerán aquí",
|
||||
"toasts": {
|
||||
"noPrompt": "Por favor introduce un prompt",
|
||||
"noModel": "Por favor selecciona un modelo",
|
||||
"noResults": "No se generaron imágenes"
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"title": "Generación de video",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Describe el video que quieres generar...",
|
||||
"duration": "Duración (s)",
|
||||
"fps": "FPS",
|
||||
"size": "Tamaño",
|
||||
"advanced": "Configuración avanzada",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Aleatorio",
|
||||
"frames": "Fotogramas",
|
||||
"referenceMedia": "Medios de referencia",
|
||||
"startImage": "Imagen inicial",
|
||||
"endImage": "Imagen final",
|
||||
"avatarAudio": "Audio del avatar"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generar video",
|
||||
"generating": "Generando..."
|
||||
},
|
||||
"empty": "Los videos generados aparecerán aquí",
|
||||
"toasts": {
|
||||
"noPrompt": "Por favor introduce un prompt",
|
||||
"noModel": "Por favor selecciona un modelo",
|
||||
"noResults": "No se generaron videos"
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"title": "Texto a voz",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"voice": "Voz",
|
||||
"voicePlaceholder": "ID de voz opcional",
|
||||
"input": "Texto",
|
||||
"inputPlaceholder": "Introduce el texto a sintetizar...",
|
||||
"instructions": "Instrucciones",
|
||||
"instructionsPlaceholder": "Por ejemplo: Habla despacio y con calidez",
|
||||
"instructionsHint": "Indicaciones opcionales sobre la locución. La compatibilidad depende del backend."
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generar audio",
|
||||
"generating": "Generando..."
|
||||
},
|
||||
"empty": "El audio generado aparecerá aquí",
|
||||
"toasts": {
|
||||
"noText": "Por favor introduce texto",
|
||||
"noModel": "Por favor selecciona un modelo",
|
||||
"generateFailed": "Generación fallida"
|
||||
}
|
||||
},
|
||||
"sound": {
|
||||
"title": "Generación de sonido",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Describe el sonido que quieres generar...",
|
||||
"duration": "Duración (s)",
|
||||
"language": "Idioma",
|
||||
"vocalLanguage": "Idioma vocal",
|
||||
"lyrics": "Letras (opcional)",
|
||||
"lyricsPlaceholder": "Letras para generación vocal",
|
||||
"advanced": "Configuración avanzada",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Aleatorio"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generar",
|
||||
"generating": "Generando..."
|
||||
},
|
||||
"empty": "El audio generado aparecerá aquí",
|
||||
"toasts": {
|
||||
"noPrompt": "Por favor introduce un prompt",
|
||||
"noModel": "Por favor selecciona un modelo",
|
||||
"generateFailed": "Generación fallida"
|
||||
}
|
||||
},
|
||||
"talk": {
|
||||
"title": "Hablar",
|
||||
"subtitle": "Conversación de voz en tiempo real",
|
||||
"actions": {
|
||||
"start": "Iniciar sesión",
|
||||
"stop": "Terminar sesión",
|
||||
"connecting": "Conectando...",
|
||||
"muted": "Silenciado",
|
||||
"mute": "Silenciar",
|
||||
"unmute": "Activar sonido"
|
||||
},
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"voice": "Voz",
|
||||
"voicePlaceholder": "alloy",
|
||||
"language": "Idioma",
|
||||
"languagePlaceholder": "es",
|
||||
"instructions": "Instrucciones",
|
||||
"instructionsPlaceholder": "Define la personalidad del asistente..."
|
||||
},
|
||||
"status": {
|
||||
"idle": "Inactivo",
|
||||
"connecting": "Conectando...",
|
||||
"listening": "Escuchando...",
|
||||
"speaking": "Hablando...",
|
||||
"ended": "Sesión finalizada"
|
||||
},
|
||||
"toasts": {
|
||||
"noModel": "Selecciona un modelo primero",
|
||||
"connectFailed": "Error al conectar: {{message}}"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"title": "Historial",
|
||||
"empty": "Sin historial aún",
|
||||
"deleteEntry": "Eliminar entrada",
|
||||
"clear": "Borrar historial",
|
||||
"clearTitle": "Borrar todo el historial",
|
||||
"clearMessage": "¿Eliminar todas las entradas del historial? Esto no se puede deshacer.",
|
||||
"clearConfirm": "Borrar",
|
||||
"cleared": "Historial borrado"
|
||||
},
|
||||
"request": {
|
||||
"heading": "Request",
|
||||
"copyCurl": "Copy as curl",
|
||||
"copied": "Copied"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"studio": {
|
||||
"tabs": {
|
||||
"images": "Gambar",
|
||||
"video": "Video",
|
||||
"tts": "TTS",
|
||||
"sound": "Suara",
|
||||
"transform": "Transformasi",
|
||||
"overview": "Ikhtisar"
|
||||
},
|
||||
"overview": {
|
||||
"eyebrow": "{{ready}} dari {{total}} modalitas siap",
|
||||
"title": "Studio",
|
||||
"subtitle": "Hasilkan gambar, video, 3D dan suara dengan model pada mesin ini.",
|
||||
"canMake": "Apa yang dapat Anda buat",
|
||||
"running": "Sedang berjalan",
|
||||
"recent": "Output terbaru",
|
||||
"noModel": "Tidak ada model yang terinstal",
|
||||
"install": "Instal model",
|
||||
"ready": "Siap",
|
||||
"seconds": "{{seconds}}s",
|
||||
"describe": {
|
||||
"images": "Teks ke gambar, gambar ke gambar, gambar referensi",
|
||||
"video": "Teks ke video dan gambar ke video",
|
||||
"threed": "Rekonstruksi mesh dari gambar",
|
||||
"tts": "Teks ke suara menggunakan pustaka suara Anda",
|
||||
"sound": "Musik dan efek suara dari prompt",
|
||||
"transform": "Pemisahan, peningkatan dan konversi suara"
|
||||
}
|
||||
},
|
||||
"groups": {
|
||||
"create": "Buat",
|
||||
"voice": "Suara",
|
||||
"transform": "Transformasi"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"title": "Pembuatan Gambar",
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Deskripsikan gambar yang ingin Anda buat...",
|
||||
"negativePrompt": "Prompt Negatif",
|
||||
"negativePromptPlaceholder": "Apa yang harus dihindari...",
|
||||
"size": "Ukuran",
|
||||
"count": "Jumlah (1-4)",
|
||||
"advanced": "Pengaturan Tingkat Lanjutan",
|
||||
"imageInputs": "Input Gambar",
|
||||
"steps": "Steps",
|
||||
"stepsPlaceholder": "20",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Acak",
|
||||
"sourceImage": "Gambar Sumber (img2img)",
|
||||
"refImages": "Gambar Referensi",
|
||||
"refImagesAdded_one": "{{count}} gambar ditambahkan",
|
||||
"refImagesAdded_other": "{{count}} gambar ditambahkan"
|
||||
},
|
||||
"actions": {
|
||||
"view": "Lihat",
|
||||
"generate": "Hasilkan",
|
||||
"generating": "Menghasilkan..."
|
||||
},
|
||||
"empty": "Gambar yang dihasilkan akan muncul di sini",
|
||||
"toasts": {
|
||||
"noPrompt": "Silahkan masukkan prompt",
|
||||
"noModel": "Silahkan pilih model",
|
||||
"noResults": "Tidak ada gambar yang dihasilkan"
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"title": "Pembuatan Video",
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Deskripsikan video yang ingin Anda buat...",
|
||||
"duration": "Durasi (detik)",
|
||||
"fps": "FPS",
|
||||
"size": "Ukuran",
|
||||
"advanced": "Pengaturan Tingkat Lanjutan",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Acak",
|
||||
"frames": "Frame",
|
||||
"referenceMedia": "Media referensi",
|
||||
"startImage": "Gambar awal",
|
||||
"endImage": "Gambar akhir",
|
||||
"avatarAudio": "Audio avatar"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Hasilkan",
|
||||
"generating": "Menghasilkan..."
|
||||
},
|
||||
"empty": "Video yang dihasilkan akan muncul di sini",
|
||||
"toasts": {
|
||||
"noPrompt": "Silahkan masukkan prompt",
|
||||
"noModel": "Silahkan pilih model",
|
||||
"noResults": "Tidak ada video yang dihasilkan"
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"title": "Teks ke Suara",
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"voice": "Suara",
|
||||
"voicePlaceholder": "ID suara opsional",
|
||||
"input": "Teks",
|
||||
"inputPlaceholder": "Masukkan teks untuk disintesis...",
|
||||
"instructions": "Instruksi",
|
||||
"instructionsPlaceholder": "Contoh: Ucapkan perlahan dan hangat",
|
||||
"instructionsHint": "Panduan penyampaian opsional. Dukungan bergantung pada backend."
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Hasilkan",
|
||||
"generating": "Menghasilkan..."
|
||||
},
|
||||
"empty": "Audio yang dihasilkan akan muncul di sini",
|
||||
"toasts": {
|
||||
"noText": "Silahkan masukkan teks",
|
||||
"noModel": "Silahkan pilih model",
|
||||
"generateFailed": "Gagal menghasilkan"
|
||||
}
|
||||
},
|
||||
"sound": {
|
||||
"title": "Pembuatan Suara",
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Deskripsikan suara yang ingin Anda buat...",
|
||||
"instrumental": "Instrumental",
|
||||
"bpm": "Tempo",
|
||||
"duration": "Durasi (detik)",
|
||||
"language": "Bahasa",
|
||||
"timesignature": "Tanda birama",
|
||||
"keyscale": "Tangga nada",
|
||||
"vocalLanguage": "Bahasa vokal",
|
||||
"vocalLanguagePlaceholder": "cth. Bahasa Indonesia",
|
||||
"caption": "Takarir",
|
||||
"lyrics": "Lirik",
|
||||
"lyricsPlaceholder": "Lirik untuk menghasilkan vokal",
|
||||
"simple": "Sederhana",
|
||||
"advanced": "Tingkat Lanjutan",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Acak",
|
||||
"thinkMode": "Mode penalaran"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Hasilkan",
|
||||
"generating": "Menghasilkan..."
|
||||
},
|
||||
"empty": "Suara yang dihasilkan akan muncul di sini",
|
||||
"toasts": {
|
||||
"noPrompt": "Silahkan masukkan prompt",
|
||||
"noModel": "Silahkan pilih model",
|
||||
"generateFailed": "Gagal menghasilkan"
|
||||
}
|
||||
},
|
||||
"audioTransform": {
|
||||
"title": "Transformasi Audio",
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"audio": "Audio (wajib)",
|
||||
"reference": "Referensi (opsional)",
|
||||
"referenceHelp": "Sinyal loopback / ujung jauh untuk pembatalan gema, atau pembicara target untuk konversi suara. Kosongkan untuk transformasi tanpa syarat.",
|
||||
"advancedParameters": "Parameter tingkat lanjut",
|
||||
"advancedParametersHelp": "Spesifik backend (satu key=value per baris, contoh: noise_gate=true)",
|
||||
"advancedParametersPlaceholder": "# Opsional. Untuk LocalVQE:\n# noise_gate=true\n# noise_gate_threshold_dbfs=-50"
|
||||
},
|
||||
"input": {
|
||||
"upload": "Unggah",
|
||||
"uploadDescription": "Tarik file di sini atau",
|
||||
"uploadBrowse": "jelajahi",
|
||||
"record": "Rekam",
|
||||
"startRecording": "Mulai merekam",
|
||||
"stop": "Selesai",
|
||||
"encoding": "Pengenkodean...",
|
||||
"clear": "Hapus",
|
||||
"microphoneUnavailable": "Perekaman mikrofon tidak tersedia di peramban ini.",
|
||||
"echoNotice": "Peramban sering kali menerapkan pembatalan gema dan penekanan derau WebRTC secara bawaan. Hal ini biasanya menghasilkan performa yang lebih buruk dibandingkan menjalankan LocalVQE pada audio mentah.",
|
||||
"echoTest": "Tes gema (rekam mik saat memutar referensi)",
|
||||
"stopEchoTest": "Hentikan tes gema"
|
||||
},
|
||||
"result": {
|
||||
"audio": "Audio",
|
||||
"reference": "Referensi",
|
||||
"inputSpectrum": "Input spektrum",
|
||||
"outputSpectrum": "Output spektrum",
|
||||
"outputSpectrumHint": "Transformasi untuk membandingkan atenuasi",
|
||||
"output": "Output"
|
||||
},
|
||||
"actions": {
|
||||
"transform": "Transformasi",
|
||||
"processing": "Memproses..."
|
||||
},
|
||||
"empty": "Pilih file audio (dan referensi opsional) untuk ditransformasikan"
|
||||
},
|
||||
"talk": {
|
||||
"title": "Percakapan",
|
||||
"subtitle": "Percakapan suara realtime",
|
||||
"actions": {
|
||||
"start": "Mulai sesi",
|
||||
"stop": "Akhiri sesi",
|
||||
"connecting": "Menghubungkan...",
|
||||
"muted": "Disenyapkan",
|
||||
"mute": "Senyapkan",
|
||||
"unmute": "Aktifkan Suara"
|
||||
},
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"voice": "Suara",
|
||||
"voicePlaceholder": "alloy",
|
||||
"language": "Bahasa",
|
||||
"languagePlaceholder": "en",
|
||||
"instructions": "Instruksi",
|
||||
"instructionsPlaceholder": "Atur persona asisten..."
|
||||
},
|
||||
"status": {
|
||||
"idle": "Idle",
|
||||
"connecting": "Menghubungkan...",
|
||||
"listening": "Mendengarkan...",
|
||||
"speaking": "Berbicara...",
|
||||
"ended": "Sesi berakhir"
|
||||
},
|
||||
"toasts": {
|
||||
"noModel": "Silahkan pilih model terlebih dahulu",
|
||||
"connectFailed": "Gagal terhubung: {{message}}"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"title": "Riwayat",
|
||||
"empty": "Tidak ada riwayat",
|
||||
"deleteEntry": "Hapus entri",
|
||||
"clear": "Hapus riwayat",
|
||||
"clearTitle": "Hapus semua riwayat",
|
||||
"clearMessage": "Hapus semua entri riwayat? Tindakan ini tidak dapat dibatalkan.",
|
||||
"clearConfirm": "Hapus",
|
||||
"cleared": "Riwayat dihapus"
|
||||
},
|
||||
"request": {
|
||||
"heading": "Request",
|
||||
"copyCurl": "Copy as curl",
|
||||
"copied": "Copied"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"studio": {
|
||||
"tabs": {
|
||||
"images": "Immagini",
|
||||
"video": "Video",
|
||||
"tts": "TTS",
|
||||
"sound": "Audio",
|
||||
"transform": "Transform",
|
||||
"overview": "Overview"
|
||||
},
|
||||
"overview": {
|
||||
"eyebrow": "{{ready}} of {{total}} modalities ready",
|
||||
"title": "Studio",
|
||||
"subtitle": "Generate images, video, 3D, speech and sound with the models on this machine.",
|
||||
"canMake": "What you can make",
|
||||
"running": "Running now",
|
||||
"recent": "Recent outputs",
|
||||
"noModel": "No model installed",
|
||||
"install": "Install a model",
|
||||
"ready": "Ready",
|
||||
"seconds": "{{seconds}}s",
|
||||
"describe": {
|
||||
"images": "Text to image, image to image, reference images",
|
||||
"video": "Text to video and image to video",
|
||||
"threed": "Image to mesh reconstruction",
|
||||
"tts": "Text to speech using your voice library",
|
||||
"sound": "Music and sound effects from a prompt",
|
||||
"transform": "Separation, enhancement and voice conversion"
|
||||
}
|
||||
},
|
||||
"groups": {
|
||||
"create": "Create",
|
||||
"voice": "Voice",
|
||||
"transform": "Transform"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"title": "Generazione immagini",
|
||||
"labels": {
|
||||
"model": "Modello",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Descrivi l'immagine che vuoi generare...",
|
||||
"negativePrompt": "Prompt negativo",
|
||||
"negativePromptPlaceholder": "Cosa evitare...",
|
||||
"size": "Dimensione",
|
||||
"count": "Quantità (1-4)",
|
||||
"advanced": "Impostazioni avanzate",
|
||||
"imageInputs": "Input immagini",
|
||||
"steps": "Passi",
|
||||
"stepsPlaceholder": "20",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Casuale",
|
||||
"sourceImage": "Immagine sorgente (img2img)",
|
||||
"refImages": "Immagini di riferimento",
|
||||
"refImagesAdded_one": "{{count}} immagine aggiunta",
|
||||
"refImagesAdded_other": "{{count}} immagini aggiunte"
|
||||
},
|
||||
"actions": {
|
||||
"view": "Visualizza",
|
||||
"generate": "Genera",
|
||||
"generating": "Generazione..."
|
||||
},
|
||||
"empty": "Le immagini generate appariranno qui",
|
||||
"toasts": {
|
||||
"noPrompt": "Inserisci un prompt",
|
||||
"noModel": "Seleziona un modello",
|
||||
"noResults": "Nessuna immagine generata"
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"title": "Generazione video",
|
||||
"labels": {
|
||||
"model": "Modello",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Descrivi il video che vuoi generare...",
|
||||
"duration": "Durata (s)",
|
||||
"fps": "FPS",
|
||||
"size": "Dimensione",
|
||||
"advanced": "Impostazioni avanzate",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Casuale",
|
||||
"frames": "Fotogrammi",
|
||||
"referenceMedia": "Media di riferimento",
|
||||
"startImage": "Immagine iniziale",
|
||||
"endImage": "Immagine finale",
|
||||
"avatarAudio": "Audio avatar"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Genera video",
|
||||
"generating": "Generazione..."
|
||||
},
|
||||
"empty": "I video generati appariranno qui",
|
||||
"toasts": {
|
||||
"noPrompt": "Inserisci un prompt",
|
||||
"noModel": "Seleziona un modello",
|
||||
"noResults": "Nessun video generato"
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"title": "Text to Speech",
|
||||
"labels": {
|
||||
"model": "Modello",
|
||||
"voice": "Voce",
|
||||
"voicePlaceholder": "ID voce opzionale",
|
||||
"input": "Testo",
|
||||
"inputPlaceholder": "Inserisci il testo da convertire in voce...",
|
||||
"instructions": "Istruzioni",
|
||||
"instructionsPlaceholder": "Ad esempio: Parla lentamente e con calore",
|
||||
"instructionsHint": "Indicazioni facoltative sullo stile. Il supporto dipende dal backend."
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Genera audio",
|
||||
"generating": "Generazione..."
|
||||
},
|
||||
"empty": "L'audio generato apparirà qui",
|
||||
"toasts": {
|
||||
"noText": "Inserisci del testo",
|
||||
"noModel": "Seleziona un modello",
|
||||
"generateFailed": "Generazione fallita"
|
||||
}
|
||||
},
|
||||
"sound": {
|
||||
"title": "Generazione audio",
|
||||
"labels": {
|
||||
"model": "Modello",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Descrivi il suono che vuoi generare...",
|
||||
"duration": "Durata (s)",
|
||||
"language": "Lingua",
|
||||
"vocalLanguage": "Lingua vocale",
|
||||
"lyrics": "Testi (opzionale)",
|
||||
"lyricsPlaceholder": "Testi per la generazione vocale",
|
||||
"advanced": "Impostazioni avanzate",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Casuale"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Genera",
|
||||
"generating": "Generazione..."
|
||||
},
|
||||
"empty": "L'audio generato apparirà qui",
|
||||
"toasts": {
|
||||
"noPrompt": "Inserisci un prompt",
|
||||
"noModel": "Seleziona un modello",
|
||||
"generateFailed": "Generazione fallita"
|
||||
}
|
||||
},
|
||||
"talk": {
|
||||
"title": "Conversazione",
|
||||
"subtitle": "Conversazione vocale in tempo reale",
|
||||
"actions": {
|
||||
"start": "Avvia sessione",
|
||||
"stop": "Termina sessione",
|
||||
"connecting": "Connessione...",
|
||||
"muted": "Muto",
|
||||
"mute": "Disattiva microfono",
|
||||
"unmute": "Riattiva microfono"
|
||||
},
|
||||
"labels": {
|
||||
"model": "Modello",
|
||||
"voice": "Voce",
|
||||
"voicePlaceholder": "alloy",
|
||||
"language": "Lingua",
|
||||
"languagePlaceholder": "it",
|
||||
"instructions": "Istruzioni",
|
||||
"instructionsPlaceholder": "Imposta la personalità dell'assistente..."
|
||||
},
|
||||
"status": {
|
||||
"idle": "Inattivo",
|
||||
"connecting": "Connessione...",
|
||||
"listening": "In ascolto...",
|
||||
"speaking": "Sta parlando...",
|
||||
"ended": "Sessione terminata"
|
||||
},
|
||||
"toasts": {
|
||||
"noModel": "Seleziona prima un modello",
|
||||
"connectFailed": "Connessione fallita: {{message}}"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"title": "Cronologia",
|
||||
"empty": "Nessuna cronologia",
|
||||
"deleteEntry": "Elimina voce",
|
||||
"clear": "Cancella cronologia",
|
||||
"clearTitle": "Cancella tutta la cronologia",
|
||||
"clearMessage": "Rimuovere tutte le voci della cronologia? Questa azione non può essere annullata.",
|
||||
"clearConfirm": "Cancella",
|
||||
"cleared": "Cronologia cancellata"
|
||||
},
|
||||
"request": {
|
||||
"heading": "Request",
|
||||
"copyCurl": "Copy as curl",
|
||||
"copied": "Copied"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"studio": {
|
||||
"tabs": {
|
||||
"images": "이미지",
|
||||
"video": "비디오",
|
||||
"tts": "TTS",
|
||||
"sound": "사운드",
|
||||
"transform": "Transform",
|
||||
"overview": "Overview"
|
||||
},
|
||||
"overview": {
|
||||
"eyebrow": "{{ready}} of {{total}} modalities ready",
|
||||
"title": "Studio",
|
||||
"subtitle": "Generate images, video, 3D, speech and sound with the models on this machine.",
|
||||
"canMake": "What you can make",
|
||||
"running": "Running now",
|
||||
"recent": "Recent outputs",
|
||||
"noModel": "No model installed",
|
||||
"install": "Install a model",
|
||||
"ready": "Ready",
|
||||
"seconds": "{{seconds}}s",
|
||||
"describe": {
|
||||
"images": "Text to image, image to image, reference images",
|
||||
"video": "Text to video and image to video",
|
||||
"threed": "Image to mesh reconstruction",
|
||||
"tts": "Text to speech using your voice library",
|
||||
"sound": "Music and sound effects from a prompt",
|
||||
"transform": "Separation, enhancement and voice conversion"
|
||||
}
|
||||
},
|
||||
"groups": {
|
||||
"create": "Create",
|
||||
"voice": "Voice",
|
||||
"transform": "Transform"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"title": "이미지 생성",
|
||||
"labels": {
|
||||
"model": "모델",
|
||||
"prompt": "프롬프트",
|
||||
"promptPlaceholder": "생성할 이미지를 설명하세요...",
|
||||
"negativePrompt": "네거티브 프롬프트",
|
||||
"negativePromptPlaceholder": "피할 요소...",
|
||||
"size": "크기",
|
||||
"count": "개수 (1-4)",
|
||||
"advanced": "고급 설정",
|
||||
"imageInputs": "이미지 입력",
|
||||
"steps": "스텝",
|
||||
"stepsPlaceholder": "20",
|
||||
"seed": "시드",
|
||||
"seedPlaceholder": "랜덤",
|
||||
"sourceImage": "원본 이미지 (img2img)",
|
||||
"refImages": "참조 이미지",
|
||||
"refImagesAdded_one": "이미지 {{count}}개 추가됨",
|
||||
"refImagesAdded_other": "이미지 {{count}}개 추가됨"
|
||||
},
|
||||
"actions": {
|
||||
"view": "View",
|
||||
"generate": "생성",
|
||||
"generating": "생성 중..."
|
||||
},
|
||||
"empty": "생성된 이미지가 여기에 표시됩니다",
|
||||
"toasts": {
|
||||
"noPrompt": "프롬프트를 입력해 주세요",
|
||||
"noModel": "모델을 선택해 주세요",
|
||||
"noResults": "생성된 이미지가 없습니다"
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"title": "비디오 생성",
|
||||
"labels": {
|
||||
"model": "모델",
|
||||
"prompt": "프롬프트",
|
||||
"promptPlaceholder": "생성할 비디오를 설명하세요...",
|
||||
"duration": "길이 (초)",
|
||||
"fps": "FPS",
|
||||
"size": "크기",
|
||||
"advanced": "고급 설정",
|
||||
"seed": "시드",
|
||||
"seedPlaceholder": "랜덤",
|
||||
"frames": "프레임",
|
||||
"referenceMedia": "참조 미디어",
|
||||
"startImage": "시작 이미지",
|
||||
"endImage": "종료 이미지",
|
||||
"avatarAudio": "아바타 오디오"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "생성",
|
||||
"generating": "생성 중..."
|
||||
},
|
||||
"empty": "생성된 비디오가 여기에 표시됩니다",
|
||||
"toasts": {
|
||||
"noPrompt": "프롬프트를 입력해 주세요",
|
||||
"noModel": "모델을 선택해 주세요",
|
||||
"noResults": "생성된 비디오가 없습니다"
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"title": "텍스트 음성 변환",
|
||||
"labels": {
|
||||
"model": "모델",
|
||||
"voice": "음성",
|
||||
"voicePlaceholder": "음성 ID (선택 사항)",
|
||||
"input": "텍스트",
|
||||
"inputPlaceholder": "합성할 텍스트를 입력하세요...",
|
||||
"instructions": "지침",
|
||||
"instructionsPlaceholder": "예: 천천히 따뜻한 어조로 말하기",
|
||||
"instructionsHint": "선택 사항인 말하기 지침입니다. 지원 여부는 백엔드에 따라 다릅니다."
|
||||
},
|
||||
"actions": {
|
||||
"generate": "생성",
|
||||
"generating": "생성 중..."
|
||||
},
|
||||
"empty": "생성된 오디오가 여기에 표시됩니다",
|
||||
"toasts": {
|
||||
"noText": "텍스트를 입력해 주세요",
|
||||
"noModel": "모델을 선택해 주세요",
|
||||
"generateFailed": "생성에 실패했습니다"
|
||||
}
|
||||
},
|
||||
"sound": {
|
||||
"title": "사운드 생성",
|
||||
"labels": {
|
||||
"model": "모델",
|
||||
"prompt": "프롬프트",
|
||||
"promptPlaceholder": "생성할 사운드를 설명하세요...",
|
||||
"duration": "길이 (초)",
|
||||
"language": "언어",
|
||||
"vocalLanguage": "보컬 언어",
|
||||
"lyrics": "가사 (선택 사항)",
|
||||
"lyricsPlaceholder": "보컬 생성을 위한 가사",
|
||||
"advanced": "고급 설정",
|
||||
"seed": "시드",
|
||||
"seedPlaceholder": "랜덤"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "생성",
|
||||
"generating": "생성 중..."
|
||||
},
|
||||
"empty": "생성된 오디오가 여기에 표시됩니다",
|
||||
"toasts": {
|
||||
"noPrompt": "프롬프트를 입력해 주세요",
|
||||
"noModel": "모델을 선택해 주세요",
|
||||
"generateFailed": "생성에 실패했습니다"
|
||||
}
|
||||
},
|
||||
"talk": {
|
||||
"title": "대화",
|
||||
"subtitle": "실시간 음성 대화",
|
||||
"actions": {
|
||||
"start": "세션 시작",
|
||||
"stop": "세션 종료",
|
||||
"connecting": "연결 중...",
|
||||
"muted": "음소거됨",
|
||||
"mute": "음소거",
|
||||
"unmute": "음소거 해제"
|
||||
},
|
||||
"labels": {
|
||||
"model": "모델",
|
||||
"voice": "음성",
|
||||
"voicePlaceholder": "alloy",
|
||||
"language": "언어",
|
||||
"languagePlaceholder": "en",
|
||||
"instructions": "지침",
|
||||
"instructionsPlaceholder": "어시스턴트의 페르소나를 설정하세요..."
|
||||
},
|
||||
"status": {
|
||||
"idle": "대기 중",
|
||||
"connecting": "연결 중...",
|
||||
"listening": "듣는 중...",
|
||||
"speaking": "말하는 중...",
|
||||
"ended": "세션이 종료되었습니다"
|
||||
},
|
||||
"toasts": {
|
||||
"noModel": "먼저 모델을 선택하세요",
|
||||
"connectFailed": "연결에 실패했습니다: {{message}}"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"title": "기록",
|
||||
"empty": "아직 기록이 없습니다",
|
||||
"deleteEntry": "항목 삭제",
|
||||
"clear": "기록 지우기",
|
||||
"clearTitle": "모든 기록 지우기",
|
||||
"clearMessage": "모든 기록 항목을 제거하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
|
||||
"clearConfirm": "지우기",
|
||||
"cleared": "기록이 지워졌습니다"
|
||||
},
|
||||
"request": {
|
||||
"heading": "Request",
|
||||
"copyCurl": "Copy as curl",
|
||||
"copied": "Copied"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
{
|
||||
"studio": {
|
||||
"tabs": {
|
||||
"images": "Imagens",
|
||||
"video": "Vídeo",
|
||||
"tts": "TTS",
|
||||
"sound": "Som",
|
||||
"transform": "Transformar",
|
||||
"threed": "3D",
|
||||
"overview": "Visão geral"
|
||||
},
|
||||
"overview": {
|
||||
"eyebrow": "{{ready}} de {{total}} modalidades prontas",
|
||||
"title": "Studio",
|
||||
"subtitle": "Gere imagens, vídeos, 3D, fala e som com os modelos desta máquina.",
|
||||
"canMake": "O que você pode criar",
|
||||
"running": "Em execução agora",
|
||||
"recent": "Saídas recentes",
|
||||
"noModel": "Nenhum modelo instalado",
|
||||
"install": "Instalar um modelo",
|
||||
"ready": "Pronto",
|
||||
"seconds": "{{seconds}}s",
|
||||
"describe": {
|
||||
"images": "Texto para imagem, imagem para imagem, imagens de referência",
|
||||
"video": "Texto para vídeo e imagem para vídeo",
|
||||
"threed": "Reconstrução de malha a partir de imagem",
|
||||
"tts": "Texto para fala usando sua biblioteca de vozes",
|
||||
"sound": "Música e efeitos sonoros a partir de um prompt",
|
||||
"transform": "Separação, aprimoramento e conversão de voz"
|
||||
}
|
||||
},
|
||||
"groups": {
|
||||
"create": "Criar",
|
||||
"voice": "Voz",
|
||||
"transform": "Transformar"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"title": "Geração de Imagens",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Descreva a imagem que deseja gerar...",
|
||||
"negativePrompt": "Prompt Negativo",
|
||||
"negativePromptPlaceholder": "O que evitar...",
|
||||
"size": "Tamanho",
|
||||
"count": "Quantidade (1-4)",
|
||||
"advanced": "Configurações Avançadas",
|
||||
"imageInputs": "Entradas de Imagem",
|
||||
"steps": "Etapas",
|
||||
"stepsPlaceholder": "20",
|
||||
"seed": "Semente",
|
||||
"seedPlaceholder": "Aleatória",
|
||||
"sourceImage": "Imagem de Origem (img2img)",
|
||||
"refImages": "Imagens de Referência",
|
||||
"refImagesAdded_one": "{{count}} imagem adicionada",
|
||||
"refImagesAdded_other": "{{count}} imagens adicionadas"
|
||||
},
|
||||
"actions": {
|
||||
"view": "Ver",
|
||||
"generate": "Gerar",
|
||||
"generating": "Gerando..."
|
||||
},
|
||||
"empty": "As imagens geradas aparecerão aqui",
|
||||
"toasts": {
|
||||
"noPrompt": "Digite um prompt",
|
||||
"noModel": "Selecione um modelo",
|
||||
"noResults": "Nenhuma imagem gerada"
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"title": "Geração de Vídeo",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Descreva o vídeo que deseja gerar...",
|
||||
"duration": "Duração (segundos)",
|
||||
"fps": "FPS",
|
||||
"size": "Tamanho",
|
||||
"advanced": "Configurações Avançadas",
|
||||
"seed": "Semente",
|
||||
"seedPlaceholder": "Aleatória",
|
||||
"frames": "Quadros",
|
||||
"referenceMedia": "Mídia de referência",
|
||||
"startImage": "Imagem inicial",
|
||||
"endImage": "Imagem final",
|
||||
"avatarAudio": "Áudio do avatar"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Gerar",
|
||||
"generating": "Gerando..."
|
||||
},
|
||||
"empty": "O vídeo gerado aparecerá aqui",
|
||||
"toasts": {
|
||||
"noPrompt": "Digite um prompt",
|
||||
"noModel": "Selecione um modelo",
|
||||
"noResults": "Nenhum vídeo gerado"
|
||||
}
|
||||
},
|
||||
"threed": {
|
||||
"title": "Geração 3D",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"image": "Imagem de entrada",
|
||||
"quality": "Qualidade",
|
||||
"quality_auto": "Automática (melhor disponível)",
|
||||
"quality_coarse": "Pré-visualização grosseira (rápida)",
|
||||
"quality_512": "512³ fino",
|
||||
"quality_1024": "Cascata 1024³ (lenta)",
|
||||
"background": "Fundo",
|
||||
"background_auto": "Remover automaticamente o fundo sólido",
|
||||
"background_keep": "Manter original",
|
||||
"background_black": "Remover preto",
|
||||
"background_white": "Remover branco",
|
||||
"advanced": "Configurações Avançadas",
|
||||
"steps": "Etapas de forma",
|
||||
"textureSteps": "Etapas de material",
|
||||
"guidance": "Orientação",
|
||||
"seed": "Semente",
|
||||
"seedPlaceholder": "Aleatória"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Gerar",
|
||||
"generating": "Gerando...",
|
||||
"remesh": "Aplicar remalhamento",
|
||||
"remeshing": "Aplicando remalhamento...",
|
||||
"showOriginal": "Mostrar original",
|
||||
"download": "Baixar GLB"
|
||||
},
|
||||
"remesh": {
|
||||
"title": "Remalhamento hermético para impressão",
|
||||
"detail": "Detalhe do remalhamento",
|
||||
"coarser": "Mais grosseiro · mais rápido",
|
||||
"finer": "Mais fino · mais lento",
|
||||
"hint": "O detalhe controla as menores características preservadas. O deslocamento de envolvimento segue automaticamente.",
|
||||
"ready": "Pré-visualizando o modelo remalhado hermético. Esta é a versão que será baixada."
|
||||
},
|
||||
"viewer": {
|
||||
"wireframe": "Wireframe",
|
||||
"autoRotate": "Rotação automática",
|
||||
"noWebgl": "WebGL2 não está disponível neste navegador — baixe o GLB para vê-lo em outro lugar.",
|
||||
"contextLost": "A visualização 3D perdeu o contexto da GPU — recarregue a página para restaurá-la.",
|
||||
"stats": "{{verts}} vértices · {{tris}} triângulos",
|
||||
"hint": "arrastar: girar · pinça/roda: zoom · dois dedos/clique direito: mover · duplo clique: redefinir"
|
||||
},
|
||||
"empty": "O modelo 3D gerado aparecerá aqui",
|
||||
"toasts": {
|
||||
"noImage": "Forneça uma imagem de entrada",
|
||||
"noModel": "Selecione um modelo",
|
||||
"noResults": "Nenhum modelo gerado"
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"title": "Texto para Fala",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"voice": "Voz",
|
||||
"voicePlaceholder": "Falante opcional ou ID de voz",
|
||||
"input": "Texto",
|
||||
"inputPlaceholder": "Digite o texto para sintetizar...",
|
||||
"instructions": "Instruções",
|
||||
"instructionsPlaceholder": "Por exemplo: Fale devagar e com calor",
|
||||
"instructionsHint": "Orientações opcionais de interpretação. O suporte depende do backend."
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Gerar",
|
||||
"generating": "Gerando..."
|
||||
},
|
||||
"empty": "O áudio gerado aparecerá aqui",
|
||||
"toasts": {
|
||||
"noText": "Digite o texto",
|
||||
"noModel": "Selecione um modelo",
|
||||
"generated": "Fala gerada",
|
||||
"generateFailed": "Falha na geração"
|
||||
},
|
||||
"voiceLibrary": {
|
||||
"cloningReady": "Clonagem de voz",
|
||||
"loading": "Carregando vozes salvas…",
|
||||
"modelDefault": "Usar padrão do modelo",
|
||||
"empty": "Nenhuma voz salva ainda.",
|
||||
"create": "Criar uma",
|
||||
"manage": "Gerenciar Biblioteca de Vozes",
|
||||
"namedVoiceHint": "Este modelo não aceita perfis de áudio de referência. Você ainda pode digitar um falante nomeado suportado pelo seu backend."
|
||||
}
|
||||
},
|
||||
"voiceLibrary": {
|
||||
"title": "Biblioteca de Vozes",
|
||||
"subtitle": "Crie e gerencie vozes de referência reutilizáveis para cada modelo instalado que suporta clonagem de voz.",
|
||||
"loading": "Carregando vozes salvas…",
|
||||
"listLabel": "Perfis de voz salvos",
|
||||
"status": {
|
||||
"ready": "Pronto"
|
||||
},
|
||||
"summary": {
|
||||
"label": "Status da biblioteca de vozes",
|
||||
"profiles": "vozes salvas",
|
||||
"modelsReady_one": "{{count}} modelo compatível pronto",
|
||||
"modelsReady_other": "{{count}} modelos compatíveis prontos",
|
||||
"noModels": "Nenhum modelo compatível instalado"
|
||||
},
|
||||
"search": {
|
||||
"label": "Buscar perfis de voz",
|
||||
"placeholder": "Buscar vozes, idiomas ou transcrições"
|
||||
},
|
||||
"filters": {
|
||||
"language": "Filtrar por idioma",
|
||||
"allLanguages": "Todos os idiomas"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Sua biblioteca de vozes está vazia",
|
||||
"body": "Grave ou envie um clipe de referência autorizado, adicione a transcrição exata e reuse-o nos modelos de TTS compatíveis."
|
||||
},
|
||||
"noResults": {
|
||||
"title": "Nenhuma voz corresponde a estes filtros",
|
||||
"body": "Tente uma busca diferente ou mostre todos os idiomas."
|
||||
},
|
||||
"detail": {
|
||||
"eyebrow": "Voz selecionada",
|
||||
"emptyTitle": "Selecione uma voz para inspecioná-la",
|
||||
"emptyBody": "Áudio de referência, transcrição, compatibilidade e detalhes de consentimento aparecerão aqui.",
|
||||
"referenceAudio": "Áudio de referência",
|
||||
"transcript": "Transcrição de referência"
|
||||
},
|
||||
"metadata": {
|
||||
"language": "Idioma",
|
||||
"languageUnknown": "Não especificado",
|
||||
"duration": "Duração",
|
||||
"sampleRate": "Taxa de amostragem",
|
||||
"created": "Criado em"
|
||||
},
|
||||
"consent": {
|
||||
"confirmed": "Consentimento confirmado",
|
||||
"confirmedAt": "Confirmado em {{date}}"
|
||||
},
|
||||
"modelSetup": {
|
||||
"title": "Instalar um modelo de clonagem de voz",
|
||||
"body": "Esta voz está pronta, mas precisa de um modelo que aceite áudio de referência. Escolha um verificado por este servidor.",
|
||||
"loading": "Encontrando modelos compatíveis em suas galerias…",
|
||||
"backendUnknown": "Backend selecionado durante a instalação",
|
||||
"install": "Instalar",
|
||||
"installing": "Instalando…",
|
||||
"loadFailed": "Recomendações compatíveis não puderam ser carregadas.",
|
||||
"noneAvailable": "Nenhum modelo compatível não instalado está disponível nas galerias configuradas.",
|
||||
"capabilityNote": "As recomendações vêm dos metadados de capacidade do modelo, não de uma lista de modelos do lado do navegador."
|
||||
},
|
||||
"api": {
|
||||
"title": "Uso da API e modelos compatíveis",
|
||||
"summary": "Use esta voz fora do navegador ou verifique o suporte do modelo",
|
||||
"body": "Envie a URI de voz estável do perfil com cada requisição de fala. O LocalAI resolve o áudio de referência privado e a transcrição no momento da geração; o YAML do modelo não é modificado.",
|
||||
"compatibleModels": "Modelos compatíveis instalados",
|
||||
"noInstalledModels": "Nenhum ainda. Instale um dos modelos verificados pelo servidor acima antes de enviar esta requisição.",
|
||||
"curlExample": "Exemplo com cURL",
|
||||
"copy": "Copiar requisição",
|
||||
"endpointNote": "Os mesmos campos de modelo, entrada e voz também funcionam com POST /tts."
|
||||
},
|
||||
"actions": {
|
||||
"create": "Criar voz",
|
||||
"createFirst": "Crie sua primeira voz",
|
||||
"retry": "Tentar novamente",
|
||||
"clearFilters": "Limpar filtros",
|
||||
"useInTTS": "Usar em Texto para Fala",
|
||||
"delete": "Excluir voz",
|
||||
"installModel": "Instalar um modelo compatível",
|
||||
"browseModels": "Explorar todos os modelos"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Excluir esta voz?",
|
||||
"message": "“{{name}}” e seu áudio de referência serão removidos permanentemente. O YAML dos modelos existentes não é alterado.",
|
||||
"deleting": "Excluindo…"
|
||||
},
|
||||
"toasts": {
|
||||
"deleted": "Voz {{name}} excluída",
|
||||
"installStarted": "Instalando {{name}}. O progresso está disponível em Operações.",
|
||||
"installFailed": "Não foi possível iniciar a instalação: {{message}}",
|
||||
"apiCopied": "Requisição de API copiada",
|
||||
"copyFailed": "Não foi possível copiar a requisição de API"
|
||||
}
|
||||
},
|
||||
"voiceCreate": {
|
||||
"eyebrow": "Biblioteca de Vozes",
|
||||
"title": "Criar uma voz reutilizável",
|
||||
"subtitle": "Adicione um clipe de referência limpo e sua transcrição exata. O LocalAI cuida dos parâmetros de clonagem específicos do backend no momento da geração.",
|
||||
"sections": {
|
||||
"reference": {
|
||||
"title": "Áudio de referência",
|
||||
"body": "Grave no navegador ou envie um clipe existente. A fala deve ser limpa, natural e de um único falante."
|
||||
},
|
||||
"details": {
|
||||
"title": "Identidade e transcrição",
|
||||
"body": "Dê aos administradores um nome reconhecível e transcreva o clipe exatamente, incluindo hesitações e pontuação."
|
||||
},
|
||||
"consent": {
|
||||
"title": "Autorização",
|
||||
"body": "A clonagem de voz pode ser sensível. Confirme a permissão antes que esta referência seja salva."
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"label": "Referência de voz",
|
||||
"normalizing": "Preparando um PCM WAV seguro…",
|
||||
"preview": "Referência normalizada",
|
||||
"durationError": "O áudio de referência deve ter entre 1 segundo e 2 minutos.",
|
||||
"decodeError": "Este navegador não conseguiu decodificar o arquivo de áudio selecionado.",
|
||||
"qualityReady": "Duração recomendada",
|
||||
"qualityHint": "6–30 segundos geralmente produzem um clone mais forte"
|
||||
},
|
||||
"fields": {
|
||||
"name": "Nome da voz",
|
||||
"namePlaceholder": "ex.: Narrador de documentário",
|
||||
"language": "Idioma (opcional)",
|
||||
"languagePlaceholder": "ex.: pt-BR",
|
||||
"description": "Descrição (opcional)",
|
||||
"descriptionPlaceholder": "Tom, origem ou uso pretendido",
|
||||
"transcript": "Transcrição exata",
|
||||
"transcriptPlaceholder": "Digite cada palavra falada no clipe de referência…",
|
||||
"transcriptHint": "Corresponda à gravação exatamente. Não reescreva a gramática nem remova pausas e hesitações."
|
||||
},
|
||||
"consent": {
|
||||
"title": "Confirmo que esta voz pode ser clonada",
|
||||
"body": "Tenho a permissão do falante ou outra base legal para armazenar e usar esta gravação para fala sintetizada."
|
||||
},
|
||||
"readiness": {
|
||||
"title": "Prontidão do perfil",
|
||||
"body": "Uma voz fica disponível em Texto para Fala quando todos os itens obrigatórios estão completos.",
|
||||
"audio": "Referência PCM-WAV válida",
|
||||
"quality": "Janela de qualidade de 6–30 segundos",
|
||||
"name": "Nome de voz reconhecível",
|
||||
"transcript": "Transcrição de referência exata",
|
||||
"consent": "Autorização confirmada"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Privada por padrão. ",
|
||||
"body": "O áudio de referência é armazenado no diretório de dados do LocalAI com permissões de arquivo restritas e nunca é exposto como um caminho de pasta de modelo."
|
||||
},
|
||||
"actions": {
|
||||
"back": "Voltar para a biblioteca",
|
||||
"cancel": "Cancelar",
|
||||
"save": "Salvar voz",
|
||||
"saving": "Salvando…"
|
||||
},
|
||||
"toasts": {
|
||||
"created": "{{name}} está pronta para uso"
|
||||
}
|
||||
},
|
||||
"sound": {
|
||||
"title": "Geração de Som",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Descreva o som que deseja gerar...",
|
||||
"instrumental": "Instrumental",
|
||||
"bpm": "BPM",
|
||||
"duration": "Duração (segundos)",
|
||||
"language": "Idioma",
|
||||
"timesignature": "Fórmula de Compasso",
|
||||
"keyscale": "Tom/Escala",
|
||||
"vocalLanguage": "Idioma vocal",
|
||||
"vocalLanguagePlaceholder": "ex.: Português",
|
||||
"caption": "Legenda",
|
||||
"lyrics": "Letra",
|
||||
"lyricsPlaceholder": "Letra para geração vocal",
|
||||
"simple": "Simples",
|
||||
"advanced": "Avançado",
|
||||
"seed": "Semente",
|
||||
"seedPlaceholder": "Aleatória",
|
||||
"thinkMode": "Modo de pensamento"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Gerar",
|
||||
"generating": "Gerando..."
|
||||
},
|
||||
"empty": "O áudio gerado aparecerá aqui",
|
||||
"toasts": {
|
||||
"noPrompt": "Digite um prompt",
|
||||
"noModel": "Selecione um modelo",
|
||||
"generateFailed": "Falha na geração"
|
||||
}
|
||||
},
|
||||
"audioTransform": {
|
||||
"title": "Transformação de Áudio",
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"audio": "Áudio (obrigatório)",
|
||||
"reference": "Referência (opcional)",
|
||||
"referenceHelp": "Sinal de loopback / extremidade distante para cancelamento de eco, falante alvo para conversão de voz. Deixe vazio para transformação incondicional.",
|
||||
"advancedParameters": "Parâmetros avançados",
|
||||
"advancedParametersHelp": "específicos do backend (uma chave=valor por linha, ex.: noise_gate=true)",
|
||||
"advancedParametersPlaceholder": "# Opcional. Para LocalVQE:\n# noise_gate=true\n# noise_gate_threshold_dbfs=-50"
|
||||
},
|
||||
"input": {
|
||||
"upload": "Enviar",
|
||||
"uploadDescription": "Solte um arquivo aqui ou",
|
||||
"uploadBrowse": "navegue",
|
||||
"record": "Gravar",
|
||||
"startRecording": "Iniciar gravação",
|
||||
"stop": "Parar",
|
||||
"encoding": "Codificando...",
|
||||
"clear": "Limpar",
|
||||
"microphoneUnavailable": "A captura de microfone não está disponível neste navegador.",
|
||||
"echoNotice": "Os navegadores costumam aplicar seu próprio cancelamento de eco WebRTC e supressão de ruído por padrão. Isso geralmente resulta em desempenho pior do que executar o LocalVQE no áudio bruto.",
|
||||
"echoTest": "Teste de eco (grave o microfone enquanto reproduz a referência)",
|
||||
"stopEchoTest": "Parar teste de eco"
|
||||
},
|
||||
"result": {
|
||||
"audio": "Áudio",
|
||||
"reference": "Referência",
|
||||
"inputSpectrum": "Espectro de entrada",
|
||||
"outputSpectrum": "Espectro de saída",
|
||||
"outputSpectrumHint": "Transforme para comparar a atenuação",
|
||||
"output": "Saída"
|
||||
},
|
||||
"actions": {
|
||||
"transform": "Transformar",
|
||||
"processing": "Processando..."
|
||||
},
|
||||
"empty": "Escolha um arquivo de áudio (e referência opcional) para transformar"
|
||||
},
|
||||
"talk": {
|
||||
"title": "Falar",
|
||||
"subtitle": "Conversa de voz em tempo real",
|
||||
"actions": {
|
||||
"start": "Iniciar sessão",
|
||||
"stop": "Encerrar sessão",
|
||||
"connecting": "Conectando...",
|
||||
"muted": "Mudo",
|
||||
"mute": "Silenciar",
|
||||
"unmute": "Reativar som"
|
||||
},
|
||||
"labels": {
|
||||
"model": "Modelo",
|
||||
"voice": "Voz",
|
||||
"voicePlaceholder": "alloy",
|
||||
"language": "Idioma",
|
||||
"languagePlaceholder": "pt",
|
||||
"instructions": "Instruções",
|
||||
"instructionsPlaceholder": "Defina a persona do assistente..."
|
||||
},
|
||||
"status": {
|
||||
"idle": "Ocioso",
|
||||
"connecting": "Conectando...",
|
||||
"listening": "Ouvindo...",
|
||||
"speaking": "Falando...",
|
||||
"ended": "Sessão encerrada"
|
||||
},
|
||||
"toasts": {
|
||||
"noModel": "Selecione um modelo primeiro",
|
||||
"connectFailed": "Falha ao conectar: {{message}}"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"title": "Histórico",
|
||||
"empty": "Nenhum histórico ainda",
|
||||
"deleteEntry": "Excluir entrada",
|
||||
"clear": "Limpar histórico",
|
||||
"clearTitle": "Limpar todo o histórico",
|
||||
"clearMessage": "Remover todas as entradas do histórico? Esta ação não pode ser desfeita.",
|
||||
"clearConfirm": "Limpar",
|
||||
"cleared": "Histórico limpo"
|
||||
},
|
||||
"request": {
|
||||
"heading": "Requisição",
|
||||
"copyCurl": "Copiar como curl",
|
||||
"copied": "Copiado"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"studio": {
|
||||
"tabs": {
|
||||
"images": "图像",
|
||||
"video": "视频",
|
||||
"tts": "TTS",
|
||||
"sound": "声音",
|
||||
"transform": "Transform",
|
||||
"overview": "Overview"
|
||||
},
|
||||
"overview": {
|
||||
"eyebrow": "{{ready}} of {{total}} modalities ready",
|
||||
"title": "Studio",
|
||||
"subtitle": "Generate images, video, 3D, speech and sound with the models on this machine.",
|
||||
"canMake": "What you can make",
|
||||
"running": "Running now",
|
||||
"recent": "Recent outputs",
|
||||
"noModel": "No model installed",
|
||||
"install": "Install a model",
|
||||
"ready": "Ready",
|
||||
"seconds": "{{seconds}}s",
|
||||
"describe": {
|
||||
"images": "Text to image, image to image, reference images",
|
||||
"video": "Text to video and image to video",
|
||||
"threed": "Image to mesh reconstruction",
|
||||
"tts": "Text to speech using your voice library",
|
||||
"sound": "Music and sound effects from a prompt",
|
||||
"transform": "Separation, enhancement and voice conversion"
|
||||
}
|
||||
},
|
||||
"groups": {
|
||||
"create": "Create",
|
||||
"voice": "Voice",
|
||||
"transform": "Transform"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"title": "图像生成",
|
||||
"labels": {
|
||||
"model": "模型",
|
||||
"prompt": "提示词",
|
||||
"promptPlaceholder": "描述您想要生成的图像...",
|
||||
"negativePrompt": "负面提示词",
|
||||
"negativePromptPlaceholder": "想要避免的内容...",
|
||||
"size": "尺寸",
|
||||
"count": "数量 (1-4)",
|
||||
"advanced": "高级设置",
|
||||
"imageInputs": "图像输入",
|
||||
"steps": "步数",
|
||||
"stepsPlaceholder": "20",
|
||||
"seed": "随机种子",
|
||||
"seedPlaceholder": "随机",
|
||||
"sourceImage": "源图像 (img2img)",
|
||||
"refImages": "参考图像",
|
||||
"refImagesAdded_one": "已添加 {{count}} 张图像",
|
||||
"refImagesAdded_other": "已添加 {{count}} 张图像"
|
||||
},
|
||||
"actions": {
|
||||
"view": "View",
|
||||
"generate": "生成",
|
||||
"generating": "生成中..."
|
||||
},
|
||||
"empty": "生成的图像将显示在此处",
|
||||
"toasts": {
|
||||
"noPrompt": "请输入提示词",
|
||||
"noModel": "请选择一个模型",
|
||||
"noResults": "未生成图像"
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"title": "视频生成",
|
||||
"labels": {
|
||||
"model": "模型",
|
||||
"prompt": "提示词",
|
||||
"promptPlaceholder": "描述您想要生成的视频...",
|
||||
"duration": "时长(秒)",
|
||||
"fps": "FPS",
|
||||
"size": "尺寸",
|
||||
"advanced": "高级设置",
|
||||
"seed": "随机种子",
|
||||
"seedPlaceholder": "随机",
|
||||
"frames": "帧数",
|
||||
"referenceMedia": "参考媒体",
|
||||
"startImage": "起始图像",
|
||||
"endImage": "结束图像",
|
||||
"avatarAudio": "虚拟形象音频"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "生成视频",
|
||||
"generating": "生成中..."
|
||||
},
|
||||
"empty": "生成的视频将显示在此处",
|
||||
"toasts": {
|
||||
"noPrompt": "请输入提示词",
|
||||
"noModel": "请选择一个模型",
|
||||
"noResults": "未生成视频"
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"title": "文字转语音",
|
||||
"labels": {
|
||||
"model": "模型",
|
||||
"voice": "声音",
|
||||
"voicePlaceholder": "可选的声音 ID",
|
||||
"input": "文本",
|
||||
"inputPlaceholder": "输入要合成的文本...",
|
||||
"instructions": "朗读指令",
|
||||
"instructionsPlaceholder": "例如:缓慢、温暖地朗读",
|
||||
"instructionsHint": "可选的表达方式指导。是否支持取决于后端。"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "生成音频",
|
||||
"generating": "生成中..."
|
||||
},
|
||||
"empty": "生成的音频将显示在此处",
|
||||
"toasts": {
|
||||
"noText": "请输入文本",
|
||||
"noModel": "请选择一个模型",
|
||||
"generateFailed": "生成失败"
|
||||
}
|
||||
},
|
||||
"sound": {
|
||||
"title": "声音生成",
|
||||
"labels": {
|
||||
"model": "模型",
|
||||
"prompt": "提示词",
|
||||
"promptPlaceholder": "描述您想要生成的声音...",
|
||||
"duration": "时长(秒)",
|
||||
"language": "语言",
|
||||
"vocalLanguage": "声乐语言",
|
||||
"lyrics": "歌词(可选)",
|
||||
"lyricsPlaceholder": "用于声乐生成的歌词",
|
||||
"advanced": "高级设置",
|
||||
"seed": "随机种子",
|
||||
"seedPlaceholder": "随机"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "生成",
|
||||
"generating": "生成中..."
|
||||
},
|
||||
"empty": "生成的音频将显示在此处",
|
||||
"toasts": {
|
||||
"noPrompt": "请输入提示词",
|
||||
"noModel": "请选择一个模型",
|
||||
"generateFailed": "生成失败"
|
||||
}
|
||||
},
|
||||
"talk": {
|
||||
"title": "通话",
|
||||
"subtitle": "实时语音对话",
|
||||
"actions": {
|
||||
"start": "开始会话",
|
||||
"stop": "结束会话",
|
||||
"connecting": "连接中...",
|
||||
"muted": "已静音",
|
||||
"mute": "静音",
|
||||
"unmute": "取消静音"
|
||||
},
|
||||
"labels": {
|
||||
"model": "模型",
|
||||
"voice": "声音",
|
||||
"voicePlaceholder": "alloy",
|
||||
"language": "语言",
|
||||
"languagePlaceholder": "zh",
|
||||
"instructions": "指令",
|
||||
"instructionsPlaceholder": "设置助手的角色..."
|
||||
},
|
||||
"status": {
|
||||
"idle": "空闲",
|
||||
"connecting": "连接中...",
|
||||
"listening": "正在聆听...",
|
||||
"speaking": "正在说话...",
|
||||
"ended": "会话已结束"
|
||||
},
|
||||
"toasts": {
|
||||
"noModel": "请先选择一个模型",
|
||||
"connectFailed": "连接失败:{{message}}"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"title": "历史",
|
||||
"empty": "暂无历史",
|
||||
"deleteEntry": "删除条目",
|
||||
"clear": "清除历史",
|
||||
"clearTitle": "清除全部历史",
|
||||
"clearMessage": "删除所有历史条目?此操作无法撤销。",
|
||||
"clearConfirm": "清除",
|
||||
"cleared": "历史已清除"
|
||||
},
|
||||
"request": {
|
||||
"heading": "Request",
|
||||
"copyCurl": "Copy as curl",
|
||||
"copied": "Copied"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// The entry of the Studio add-on. The pages are the ones that used to be core
|
||||
// routes; they keep the host modules they were written against (React, the
|
||||
// router, i18next, the shared components), and the stand-ins in
|
||||
// addons/internal/host-compat answer those through the SDK. The page code is
|
||||
// loaded after the SDK is set, because a stand-in reads the SDK when it starts.
|
||||
import { requireHost, setSdk } from '../../../internal/host-compat/src/runtime.js'
|
||||
|
||||
// What the pages need from the host beyond SDK 2's first set.
|
||||
const NEEDS = [
|
||||
'ui.LoadingSpinner', 'ui.Modal', 'ui.Lightbox', 'ui.ModelSelector', 'ui.PageHeader', 'ui.ConfirmDialog',
|
||||
'ui.MediaInput', 'ui.WaveformPlayer', 'hooks.useModels', 'hooks.useResources', 'hooks.useVoiceProfiles',
|
||||
'hooks.useOperations', 'hooks.useMediaCapture', 'hooks.useObjectUrl', 'navigation.openApp',
|
||||
]
|
||||
|
||||
export async function createAddon(sdk) {
|
||||
requireHost(sdk, NEEDS)
|
||||
setSdk(sdk)
|
||||
const { createStudio } = await import('./addon.jsx')
|
||||
return createStudio(sdk)
|
||||
}
|
||||
+2
-3
@@ -7,12 +7,11 @@ import { CAP_AUDIO_TRANSFORM } from '../utils/capabilities'
|
||||
import WaveformPlayer from '../components/audio/WaveformPlayer'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Spectrogram from '../components/audio/Spectrogram'
|
||||
import { audioTransformApi } from '../utils/api'
|
||||
import { audioTransformApi, fetchUrl } from '../utils/api'
|
||||
import { useMediaCapture } from '../hooks/useMediaCapture'
|
||||
import useObjectUrl from '../hooks/useObjectUrl'
|
||||
import { useMediaHistory } from '../hooks/useMediaHistory'
|
||||
import { useStudioHandoff, useHandoffSource, blobToFile } from '../hooks/useStudioHandoff'
|
||||
import { apiUrl } from '../utils/basePath'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import HandoffNote from '../components/studio/HandoffNote'
|
||||
import {
|
||||
@@ -159,7 +158,7 @@ export default function AudioTransform() {
|
||||
selectEntry(null)
|
||||
ws.begin(next)
|
||||
const fetchFile = async (url, fallback) => {
|
||||
const res = await fetch(url.startsWith('http') || url.startsWith('blob:') ? url : apiUrl(url))
|
||||
const res = await fetchUrl(url)
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return blobToFile(await res.blob(), url.split('?')[0].split('/').pop() || fallback)
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-1
@@ -80,8 +80,9 @@ export default function StudioOverview({ modalities, upscalers, modelsLoading, m
|
||||
|
||||
return (
|
||||
<div data-testid="studio-overview" className="page-pad studio-front">
|
||||
{/* The add-on shell already titles the page "Studio", so this header
|
||||
carries the supporting line and the key hint only. */}
|
||||
<PageHeader
|
||||
title={t('studio.overview.title')}
|
||||
supporting={t('studio.overview.subtitle')}
|
||||
actions={<span className="studio-key-hint"><kbd className="dk-kbd">/</kbd> {t('studio.composer.startTyping')}</span>}
|
||||
/>
|
||||
File renamed without changes.
+2
-3
@@ -15,8 +15,7 @@ import LoadingSpinner from '../components/LoadingSpinner'
|
||||
import GlbViewer from '../components/GlbViewer'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import MediaInput from '../components/biometrics/MediaInput'
|
||||
import { threeDApi } from '../utils/api'
|
||||
import { apiUrl } from '../utils/basePath'
|
||||
import { threeDApi, fetchUrl } from '../utils/api'
|
||||
import { use3DHistory } from '../hooks/use3DHistory'
|
||||
import { useStudioHandoff, useHandoffSource, blobToImageInput } from '../hooks/useStudioHandoff'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -177,7 +176,7 @@ export default function ThreeDGen() {
|
||||
addToast(t('threed.toasts.noResults'), 'warning')
|
||||
return
|
||||
}
|
||||
const glbResp = await fetch(apiUrl(url))
|
||||
const glbResp = await fetchUrl(url)
|
||||
if (!glbResp.ok) throw new Error(`fetching the generated GLB failed: HTTP ${glbResp.status}`)
|
||||
const glb = await glbResp.blob()
|
||||
const name = url.split('/').pop()
|
||||
File renamed without changes.
@@ -0,0 +1 @@
|
||||
/* Styles of the Studio add-on. The page style sheets (components/studio/*.css) are added by the build. */
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -1,7 +1,7 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { axisTicks, clock, hasText, runLength, speakerRows, toRttm, toSrt } from './diarization.js'
|
||||
import { axisTicks, clock, hasText, runLength, speakerRows, toRttm, toSrt } from '../src/utils/diarization.js'
|
||||
|
||||
const result = {
|
||||
speakers: [{ id: 'SPEAKER_00', label: '0' }, { id: 'SPEAKER_01', label: '1', name: 'Ada' }],
|
||||
@@ -0,0 +1,53 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { hasKey, hostUi, iconLiterals, iconNames, literalKeys, sourceFiles, spriteNames, text, unresolvedImports } from '../../../internal/host-compat/test/parity.mjs'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const src = join(here, '../src')
|
||||
const files = sourceFiles(src)
|
||||
const LANGUAGES = ['en', 'it', 'es', 'de', 'zh-CN', 'id', 'ko', 'pt-BR']
|
||||
|
||||
test('the media locale files are byte copies of the host ones', () => {
|
||||
for (const lng of LANGUAGES) {
|
||||
assert.equal(
|
||||
readFileSync(join(src, `i18n/media.${lng}.json`), 'utf8'),
|
||||
readFileSync(join(hostUi, `public/locales/${lng}/media.json`), 'utf8'),
|
||||
lng,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
test('every literal key the pages read is in the English media locale', () => {
|
||||
const en = JSON.parse(readFileSync(join(src, 'i18n/media.en.json'), 'utf8'))
|
||||
const keys = literalKeys(text(files))
|
||||
assert.ok(keys.size > 150, `found only ${keys.size} literal keys`)
|
||||
const missing = [...keys].filter((k) => !hasKey(en, k))
|
||||
assert.deepEqual(missing, [])
|
||||
})
|
||||
|
||||
test('every icon the pages name is one the SDK allows', async () => {
|
||||
const allowed = await iconNames()
|
||||
const sprite = spriteNames()
|
||||
const used = iconLiterals(text(files))
|
||||
assert.ok(used.size > 20, `found only ${used.size} icons`)
|
||||
// Each is also a real sprite icon, so none draws the neutral glyph.
|
||||
assert.deepEqual([...used].filter((n) => !sprite.has(n) || !allowed.has(n)), [])
|
||||
})
|
||||
|
||||
test('the source imports only its own files, the compat stand-ins and the modules they answer', () => {
|
||||
assert.deepEqual(unresolvedImports(src), [])
|
||||
})
|
||||
|
||||
test('the source holds no direct network call: the pages go through the stand-in API client', () => {
|
||||
const direct = []
|
||||
for (const f of files) {
|
||||
if (f.endsWith('test.mjs')) continue
|
||||
for (const line of readFileSync(f, 'utf8').split('\n')) {
|
||||
if (/\bfetch\(/.test(line) && !/^\s*\/\//.test(line)) direct.push(`${f.slice(src.length)}: ${line.trim()}`)
|
||||
}
|
||||
}
|
||||
assert.deepEqual(direct, [])
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { build } from '../build.mjs'
|
||||
import { React, renderToString, makeSdk } from '../../../internal/host-compat/test/host.mjs'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
async function load(overrides) {
|
||||
const dir = await build()
|
||||
const mod = await import(pathToFileURL(join(dir, 'ui/studio.js')).href + `?t=${Date.now()}`)
|
||||
const host = await makeSdk({ addonId: 'studio', pages: ['studio'], overrides })
|
||||
return { mod, ...host }
|
||||
}
|
||||
|
||||
test('render: createAddon returns a page component and dispose', async () => {
|
||||
const { mod, sdk, controller } = await load()
|
||||
const addon = await mod.createAddon(sdk)
|
||||
assert.equal(typeof addon.Component, 'function')
|
||||
assert.equal(typeof addon.dispose, 'function')
|
||||
addon.dispose()
|
||||
controller.abort('done')
|
||||
})
|
||||
|
||||
test('render: an unknown page draws nothing', async () => {
|
||||
const { mod, sdk } = await load()
|
||||
const addon = await mod.createAddon(sdk)
|
||||
assert.equal(renderToString(React.createElement(addon.Component, { page: 'nope', path: '', search: '' })), '')
|
||||
})
|
||||
|
||||
test('render: the overview draws the tab strip with its words from the media locale', async () => {
|
||||
const { mod, sdk } = await load()
|
||||
const addon = await mod.createAddon(sdk)
|
||||
const html = renderToString(React.createElement(addon.Component, { page: 'studio', path: '', search: '' }))
|
||||
assert.match(html, /class="studio-tabs"/)
|
||||
assert.match(html, /data-tab="images"/)
|
||||
assert.match(html, /data-tab="overview"/)
|
||||
assert.doesNotMatch(html, /studio\.tabs\./, 'a key was shown instead of its words')
|
||||
})
|
||||
|
||||
test('render: every tab draws without throwing', async () => {
|
||||
const { mod, sdk } = await load()
|
||||
const addon = await mod.createAddon(sdk)
|
||||
for (const tab of ['images', 'video', 'threed', 'tts', 'sound', 'transform', 'diarization']) {
|
||||
const html = renderToString(React.createElement(addon.Component, { page: 'studio', path: tab, search: '' }))
|
||||
assert.match(html, new RegExp(`studio-tab-active[^>]*|data-tab="${tab}"`), tab)
|
||||
assert.doesNotMatch(html, /[a-z]+\.[a-z]+\.[a-zA-Z.]+<\/(span|h1|p|button|label)>/, `${tab}: a key was shown instead of its words`)
|
||||
}
|
||||
})
|
||||
|
||||
test('render: the manifest declares one page, the main page and SDK 2', () => {
|
||||
const m = JSON.parse(readFileSync(join(here, '../../manifest.json'), 'utf8'))
|
||||
assert.equal(m.id, 'studio')
|
||||
assert.deepEqual(m.capabilities.ui.pages, ['studio'])
|
||||
assert.deepEqual(m.capabilities.ui.navigation, ['studio'])
|
||||
assert.equal(m.capabilities.ui.mainPage, 'studio')
|
||||
assert.deepEqual(m.capabilities.ui.sdk, { min: 2, max: 2 })
|
||||
assert.deepEqual(m.capabilities.tools ?? [], [])
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
// Runs the node unit tests of the Studio UI package and prints one JSON line
|
||||
// per test, the same shape as the host's runner (core/http/react-ui/tests/unit):
|
||||
// {"case":"unit: <name>","status":"passed"|"failed"|"skipped"}
|
||||
import { run } from 'node:test'
|
||||
import { readdirSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const names = readdirSync(here).filter(f => f.endsWith('.test.mjs')).sort()
|
||||
let failed = false
|
||||
const stream = run({ files: names.map(f => join(here, f)), concurrency: 1 })
|
||||
stream.on('test:pass', ({ name, skip, todo, nesting }) => {
|
||||
if (nesting > 0) return
|
||||
console.log(JSON.stringify({ case: `unit: ${name}`, status: skip || todo ? 'skipped' : 'passed' }))
|
||||
})
|
||||
stream.on('test:fail', ({ name, nesting, details }) => {
|
||||
if (nesting > 0) return
|
||||
failed = true
|
||||
console.log(JSON.stringify({ case: `unit: ${name}`, status: 'failed', error: String(details && details.error && details.error.message || 'failed').slice(0, 400) }))
|
||||
})
|
||||
stream.on('end', () => { process.exit(failed ? 1 : 0) })
|
||||
stream.resume()
|
||||
+2
-2
@@ -5,10 +5,10 @@ import {
|
||||
MAX_FAVOURITES, TYPE_ORDER, canTake, collectWork, columnCount, countByType, dealColumns, filterWork,
|
||||
groupWork, handoffPath, layoutLineage, neighbour, projectOf, pruneIds, readHandoff, sourceUrlFor,
|
||||
suggestNext, suggestType, toggleId, waveBars,
|
||||
} from './studioWork.js'
|
||||
} from '../src/utils/studioWork.js'
|
||||
import {
|
||||
FAVOURITES_KEY, MEDIA_STORAGE_KEYS, clearAllMediaHistory,
|
||||
} from './mediaHistoryStore.js'
|
||||
} from '../src/utils/mediaHistoryStore.js'
|
||||
|
||||
const entry = (id, createdAt, extra = {}) => ({
|
||||
id, createdAt, prompt: `prompt ${id}`, model: 'm', params: {}, results: [{ url: `/generated/${id}.png` }], ...extra,
|
||||
+5
-2
@@ -1,8 +1,11 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { upscaleModels, sourceImageFile } from './upscale.js'
|
||||
import { toWorkItem, EDGE_KINDS } from './studioWork.js'
|
||||
import '../../../internal/host-compat/test/register.mjs'
|
||||
|
||||
// upscale.js imports a module that lives in the compat directory.
|
||||
const { upscaleModels, sourceImageFile } = await import('../src/utils/upscale.js')
|
||||
import { toWorkItem, EDGE_KINDS } from '../src/utils/studioWork.js'
|
||||
|
||||
test('only enabled upscalers with positive finite scale are offered', () => {
|
||||
assert.deepEqual(upscaleModels([
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Command talk is the executable of the Talk add-on. It serves no tools:
|
||||
// the add-on works in the browser, through the SDK and the core API.
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAI/addons/internal/idle"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(idle.Run(os.Args))
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "talk",
|
||||
"version": "0.1.0",
|
||||
"executable": "talk",
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"host": { "min": 1, "max": 1 },
|
||||
"api": { "min": 1, "max": 1 },
|
||||
"protocol": { "min": 1, "max": 1 },
|
||||
"dataFormat": { "min": 1, "max": 1 },
|
||||
"capabilities": {
|
||||
"ui": {
|
||||
"entry": "ui/talk.js",
|
||||
"css": ["ui/talk.css"],
|
||||
"sdk": { "min": 2, "max": 2 },
|
||||
"pages": ["talk"],
|
||||
"navigation": ["talk"],
|
||||
"mainPage": "talk",
|
||||
"icon": "ui/icon.png",
|
||||
"title": "Talk",
|
||||
"description": "Have a spoken conversation with a model: your voice goes in, the answer is spoken back."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# Release platform matrix of the talk add-on package.
|
||||
#
|
||||
# A platform ships only while "enabled" is true. Enable a platform in the same
|
||||
# change that shows its native check passing on a runner of that platform;
|
||||
# until then it stays listed and disabled. The release workflow fails closed
|
||||
# when no platform is enabled, and it never builds a platform that is not
|
||||
# enabled here.
|
||||
#
|
||||
# runner: the GitHub runner label that builds and checks the platform
|
||||
# natively. Required for an enabled platform.
|
||||
# native_check: "executable-start": the packaged executable is extracted on
|
||||
# the native runner and started. Required for an enabled platform.
|
||||
# runtime: files besides the executable that the platform needs at run
|
||||
# time, relative to the package root. They are taken from the
|
||||
# runtime directory of the build and must exist.
|
||||
schema: 1
|
||||
addon: talk
|
||||
executable: talk
|
||||
manifest: addons/talk/manifest.json
|
||||
# The directory whose content is laid at the package root (ui/talk.js ...).
|
||||
ui: addons/talk/ui/dist
|
||||
platforms:
|
||||
- platform: linux/amd64
|
||||
enabled: true
|
||||
runner: ubuntu-24.04
|
||||
native_check: executable-start
|
||||
- platform: linux/arm64
|
||||
enabled: true
|
||||
runner: ubuntu-24.04-arm
|
||||
native_check: executable-start
|
||||
- platform: darwin/arm64
|
||||
enabled: true
|
||||
runner: macos-14
|
||||
native_check: executable-start
|
||||
- platform: darwin/amd64
|
||||
enabled: true
|
||||
runner: macos-15-intel
|
||||
native_check: executable-start
|
||||
@@ -0,0 +1,2 @@
|
||||
# Written by build.mjs; the package build makes it.
|
||||
/dist/
|
||||
@@ -0,0 +1,15 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { buildAddonUI } from '../../internal/uibuild/uibuild.mjs'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export async function build() {
|
||||
// The page code keeps the host module names it was written with; the compat
|
||||
// directory answers them through the SDK (addons/internal/host-compat).
|
||||
return buildAddonUI({ root: here, name: 'talk', rgb: [28, 112, 104], compat: resolve(here, '../../internal/host-compat/src') })
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
build().then((dir) => console.log(`built ${dir}`), (e) => { console.error(e); process.exit(1) })
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "localai-talk-ui",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "Native UI of the Talk add-on: a standalone ES module that the host loads after its own build.",
|
||||
"type": "module",
|
||||
"scripts": { "build": "node build.mjs", "test": "node test/run.mjs" }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { RouteProvider } from 'react-router-dom'
|
||||
import { registerLocales } from '../../../internal/host-compat/src/i18n.js'
|
||||
import Talk from './pages/Talk'
|
||||
import en from './i18n/talk.en.json'
|
||||
|
||||
// The words of the page are the host's own `talk` locale file.
|
||||
registerLocales({ talk: { en } })
|
||||
|
||||
export function createTalk() {
|
||||
function Component({ page, path, search }) {
|
||||
if (page !== 'talk') return null
|
||||
return (
|
||||
<RouteProvider value={{ page, path: path || '', search: search || '' }}>
|
||||
<Talk />
|
||||
</RouteProvider>
|
||||
)
|
||||
}
|
||||
return { Component, components: {}, chatActions: [], dispose() {} }
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,17 @@
|
||||
// The entry of the Talk add-on. The page is the one that used to be a core
|
||||
// route; it keeps the host modules it was written against and the stand-ins in
|
||||
// addons/internal/host-compat answer those through the SDK (see the Studio
|
||||
// add-on's entry).
|
||||
import { requireHost, setSdk } from '../../../internal/host-compat/src/runtime.js'
|
||||
|
||||
const NEEDS = [
|
||||
'ui.HomeModelPicker', 'ui.PageHeader', 'hooks.useMCPClient', 'navigation.openApp', 'portal',
|
||||
]
|
||||
|
||||
export async function createAddon(sdk) {
|
||||
requireHost(sdk, NEEDS.filter((n) => n.includes('.')))
|
||||
if (typeof sdk.portal !== 'function') throw new Error('This add-on needs a newer LocalAI: the host has no portal')
|
||||
setSdk(sdk)
|
||||
const { createTalk } = await import('./addon.jsx')
|
||||
return createTalk(sdk)
|
||||
}
|
||||
@@ -716,7 +716,7 @@ export default function Talk() {
|
||||
return (
|
||||
<div className="talk-page" data-view={view}>
|
||||
<header className="talk-hd">
|
||||
<h1>{t('title')}</h1>
|
||||
{/* The add-on shell already titles the page "Talk". */}
|
||||
<span className="talk-hd__sub">{t('subtitle')}</span>
|
||||
<div className="talk-hd__acts">
|
||||
{isConnected && (
|
||||
@@ -10,7 +10,6 @@
|
||||
.talk-page a:focus-visible,
|
||||
.talk-page input:focus-visible { outline: 2px solid var(--dk-accent-text); outline-offset: 2px; }
|
||||
.talk-hd { display: flex; align-items: center; gap: 12px; min-height: 56px; padding: 8px 0; }
|
||||
.talk-hd h1 { margin: 0; font-family: var(--font-sans); font-size: 20px; font-weight: 650; letter-spacing: -.01em; }
|
||||
.talk-hd__sub { color: var(--dk-muted); font-family: var(--font-mono); font-size: 12px; }
|
||||
.talk-hd__acts { display: flex; align-items: center; gap: 2px; margin-left: auto; }
|
||||
.talk-icobtn {
|
||||
@@ -0,0 +1 @@
|
||||
/* Styles of the Talk add-on. The page style sheet (pages/talk.css) is added by the build. */
|
||||
@@ -0,0 +1,39 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { hasKey, iconLiterals, iconNames, literalKeys, sourceFiles, spriteNames, text, unresolvedImports } from '../../../internal/host-compat/test/parity.mjs'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const src = join(here, '../src')
|
||||
const files = sourceFiles(src)
|
||||
|
||||
test('every literal key the page reads is in the English talk locale', () => {
|
||||
const en = JSON.parse(readFileSync(join(src, 'i18n/talk.en.json'), 'utf8'))
|
||||
const keys = literalKeys(text(files))
|
||||
assert.ok(keys.size > 40, `found only ${keys.size} literal keys`)
|
||||
assert.deepEqual([...keys].filter((k) => !hasKey(en, k)), [])
|
||||
})
|
||||
|
||||
test('every icon the page names is one the SDK allows', async () => {
|
||||
const allowed = await iconNames()
|
||||
const sprite = spriteNames()
|
||||
const used = iconLiterals(text(files))
|
||||
assert.ok(used.size > 5, `found only ${used.size} icons`)
|
||||
assert.deepEqual([...used].filter((n) => !sprite.has(n) || !allowed.has(n)), [])
|
||||
})
|
||||
|
||||
test('the source imports only its own files, the compat stand-ins and the modules they answer', () => {
|
||||
assert.deepEqual(unresolvedImports(src), [])
|
||||
})
|
||||
|
||||
test('the source holds no direct network call', () => {
|
||||
const direct = []
|
||||
for (const f of files) {
|
||||
for (const line of readFileSync(f, 'utf8').split('\n')) {
|
||||
if (/\bfetch\(/.test(line) && !/^\s*\/\//.test(line)) direct.push(`${f.slice(src.length)}: ${line.trim()}`)
|
||||
}
|
||||
}
|
||||
assert.deepEqual(direct, [])
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { build } from '../build.mjs'
|
||||
import { React, renderToString, makeSdk } from '../../../internal/host-compat/test/host.mjs'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
async function load(overrides) {
|
||||
const dir = await build()
|
||||
const mod = await import(pathToFileURL(join(dir, 'ui/talk.js')).href + `?t=${Date.now()}`)
|
||||
const host = await makeSdk({ addonId: 'talk', pages: ['talk'], overrides })
|
||||
return { mod, ...host }
|
||||
}
|
||||
|
||||
test('render: createAddon returns a page component and dispose', async () => {
|
||||
const { mod, sdk, controller } = await load()
|
||||
const addon = await mod.createAddon(sdk)
|
||||
assert.equal(typeof addon.Component, 'function')
|
||||
assert.equal(typeof addon.dispose, 'function')
|
||||
addon.dispose()
|
||||
controller.abort('done')
|
||||
})
|
||||
|
||||
test('render: an unknown page draws nothing', async () => {
|
||||
const { mod, sdk } = await load()
|
||||
const addon = await mod.createAddon(sdk)
|
||||
assert.equal(renderToString(React.createElement(addon.Component, { page: 'nope', path: '', search: '' })), '')
|
||||
})
|
||||
|
||||
test('render: the page draws its title from the talk locale and no key', async () => {
|
||||
const { mod, sdk } = await load()
|
||||
const addon = await mod.createAddon(sdk)
|
||||
const html = renderToString(React.createElement(addon.Component, { page: 'talk', path: '', search: '' }))
|
||||
assert.ok(html.length > 200, 'the page drew something')
|
||||
assert.doesNotMatch(html, />(stage|picker|view|controls|transcript|settings|toasts)\.[A-Za-z.]+</, 'a key was shown instead of its words')
|
||||
})
|
||||
|
||||
test('createAddon fails readably on a host without the pieces the page needs', async () => {
|
||||
const { mod, sdk } = await load()
|
||||
const old = Object.freeze({ ...sdk, ui: Object.freeze({}) })
|
||||
await assert.rejects(mod.createAddon(old), /needs a newer LocalAI/)
|
||||
})
|
||||
|
||||
test('render: the manifest declares one page, the main page and SDK 2', () => {
|
||||
const m = JSON.parse(readFileSync(join(here, '../../manifest.json'), 'utf8'))
|
||||
assert.equal(m.id, 'talk')
|
||||
assert.deepEqual(m.capabilities.ui.pages, ['talk'])
|
||||
assert.deepEqual(m.capabilities.ui.navigation, ['talk'])
|
||||
assert.equal(m.capabilities.ui.mainPage, 'talk')
|
||||
assert.deepEqual(m.capabilities.ui.sdk, { min: 2, max: 2 })
|
||||
assert.deepEqual(m.capabilities.tools ?? [], [])
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
// Runs the node unit tests of the Talk UI package and prints one JSON line
|
||||
// per test, the same shape as the host's runner (core/http/react-ui/tests/unit):
|
||||
// {"case":"unit: <name>","status":"passed"|"failed"|"skipped"}
|
||||
import { run } from 'node:test'
|
||||
import { readdirSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const names = readdirSync(here).filter(f => f.endsWith('.test.mjs')).sort()
|
||||
let failed = false
|
||||
const stream = run({ files: names.map(f => join(here, f)), concurrency: 1 })
|
||||
stream.on('test:pass', ({ name, skip, todo, nesting }) => {
|
||||
if (nesting > 0) return
|
||||
console.log(JSON.stringify({ case: `unit: ${name}`, status: skip || todo ? 'skipped' : 'passed' }))
|
||||
})
|
||||
stream.on('test:fail', ({ name, nesting, details }) => {
|
||||
if (nesting > 0) return
|
||||
failed = true
|
||||
console.log(JSON.stringify({ case: `unit: ${name}`, status: 'failed', error: String(details && details.error && details.error.message || 'failed').slice(0, 400) }))
|
||||
})
|
||||
stream.on('end', () => { process.exit(failed ? 1 : 0) })
|
||||
stream.resume()
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAI/core/services/addons/builtin"
|
||||
"github.com/mudler/LocalAI/core/services/addons/pins"
|
||||
)
|
||||
|
||||
@@ -32,15 +33,20 @@ func pinnedIDs(t *testing.T, s pins.Store, user string) []string {
|
||||
}
|
||||
|
||||
func TestSweepNeverDeletesTheBuiltinPins(t *testing.T) {
|
||||
// No page is built in today, so the test makes up two.
|
||||
defer builtin.SetCatalogForTest([]builtin.Entry{
|
||||
{ID: "guide", Kind: builtin.KindPage, TitleKey: "items.chat", Icon: "chat", MainPath: "/app/chat"},
|
||||
{ID: "help", Kind: builtin.KindPage, TitleKey: "items.models", Icon: "boxes", MainPath: "/app/models"},
|
||||
})()
|
||||
ctx := context.Background()
|
||||
s, _ := pins.NewFileStore("")
|
||||
_ = s.Replace(ctx, "u1", []string{"studio", "talk", "notes"})
|
||||
_ = s.Replace(ctx, "u1", []string{"guide", "help", "notes"})
|
||||
t0 := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
|
||||
owner := fakeOwner{ids: []string{}}
|
||||
sweepAddonPins(ctx, s, owner, t0)
|
||||
sweepAddonPins(ctx, s, owner, t0.Add(pins.Retention+time.Hour))
|
||||
// notes is not installed and expired; the built-ins are always present.
|
||||
if got := pinnedIDs(t, s, "u1"); !slices.Equal(got, []string{"studio", "talk"}) {
|
||||
if got := pinnedIDs(t, s, "u1"); !slices.Equal(got, []string{"guide", "help"}) {
|
||||
t.Fatalf("unexpected pins %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func builtinViewer(c echo.Context, db *gorm.DB) (builtin.Viewer, bool) {
|
||||
|
||||
// ListBuiltin lists the built-in page add-ons the caller may open.
|
||||
// @Summary List the built-in add-ons the caller may open
|
||||
// @Description Returns the page add-ons that ship with LocalAI (Studio and Talk). Each entry carries only its own gate: the route needs an authenticated caller and no `addons` feature, so a user who reaches Studio today still reaches it from the Add-ons drawer.
|
||||
// @Description Returns the page add-ons that ship inside the web bundle. The list is empty today: Studio and Talk are packaged add-ons that a person installs from the Add-ons drawer. Each entry carries only its own gate: the route needs an authenticated caller and no `addons` feature.
|
||||
// @Tags AddonNavigation
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string][]builtin.Entry "Entries the caller may open"
|
||||
|
||||
@@ -8,9 +8,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mudler/LocalAI/core/services/addons/builtin"
|
||||
)
|
||||
|
||||
func TestListBuiltinWithoutAnAccountDatabaseListsEveryEntry(t *testing.T) {
|
||||
func listBuiltin(t *testing.T) (*httptest.ResponseRecorder, []string) {
|
||||
t.Helper()
|
||||
e := echo.New()
|
||||
e.GET("/api/addons/builtin", ListBuiltin(nil))
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -26,7 +28,30 @@ func TestListBuiltinWithoutAnAccountDatabaseListsEveryEntry(t *testing.T) {
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(body.Builtin) != 2 || body.Builtin[0].ID != "studio" || body.Builtin[1].ID != "talk" {
|
||||
ids := []string{}
|
||||
for _, e := range body.Builtin {
|
||||
ids = append(ids, e.ID)
|
||||
}
|
||||
return rec, ids
|
||||
}
|
||||
|
||||
func TestListBuiltinIsEmptyBecauseStudioAndTalkArePackagedAddOns(t *testing.T) {
|
||||
rec, ids := listBuiltin(t)
|
||||
if len(ids) != 0 {
|
||||
t.Fatalf("no page is built in, got %v", ids)
|
||||
}
|
||||
if rec.Header().Get("Cache-Control") != "private, no-store" {
|
||||
t.Fatalf("the list depends on the caller and must not be cached, got %q", rec.Header().Get("Cache-Control"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListBuiltinWithoutAnAccountDatabaseListsEveryEntry(t *testing.T) {
|
||||
defer builtin.SetCatalogForTest([]builtin.Entry{
|
||||
{ID: "guide", Kind: builtin.KindPage, TitleKey: "items.chat", Icon: "chat", MainPath: "/app/chat"},
|
||||
{ID: "notes-page", Kind: builtin.KindPage, TitleKey: "items.models", Icon: "boxes", MainPath: "/app/models"},
|
||||
})()
|
||||
rec, ids := listBuiltin(t)
|
||||
if len(ids) != 2 || ids[0] != "guide" || ids[1] != "notes-page" {
|
||||
t.Fatalf("unexpected body %s", rec.Body.String())
|
||||
}
|
||||
if rec.Header().Get("Cache-Control") != "private, no-store" {
|
||||
|
||||
@@ -11,19 +11,31 @@ import (
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mudler/LocalAI/core/http/auth"
|
||||
"github.com/mudler/LocalAI/core/services/addons/builtin"
|
||||
"github.com/mudler/LocalAI/core/services/addons/pins"
|
||||
)
|
||||
|
||||
// No page is built in today, so these tests make up two built-in pages to check
|
||||
// how the routes filter and keep pins.
|
||||
func madeUpPages(t *testing.T) {
|
||||
t.Helper()
|
||||
t.Cleanup(builtin.SetCatalogForTest([]builtin.Entry{
|
||||
{ID: "guide", Kind: builtin.KindPage, TitleKey: "items.chat", Icon: "chat", MainPath: "/app/chat"},
|
||||
{ID: "help", Kind: builtin.KindPage, TitleKey: "items.models", Icon: "boxes", MainPath: "/app/models"},
|
||||
}))
|
||||
}
|
||||
|
||||
// pinsApp builds the two routes over a store. With seeded set, the upgrade seed
|
||||
// has run for the local owner, as it does at start in a deployment that
|
||||
// existed before this release.
|
||||
func pinsApp(t *testing.T, seeded bool) *echo.Echo {
|
||||
madeUpPages(t)
|
||||
store, err := pins.NewFileStore("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if seeded {
|
||||
if _, err := store.SeedExisting(context.Background(), []string{auth.AddonLocalOwnerID}, pins.SeedIDs); err != nil {
|
||||
if _, err := store.SeedExisting(context.Background(), []string{auth.AddonLocalOwnerID}, []string{"guide", "help"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -55,23 +67,23 @@ func TestAnAccountCreatedAfterTheUpgradeGetsNoPins(t *testing.T) {
|
||||
func TestPinsRoundTripForTheLocalOwner(t *testing.T) {
|
||||
e := pinsApp(t, true)
|
||||
code, got := call(e, http.MethodGet, "")
|
||||
if code != 200 || len(got) != 2 || got[0] != "studio" || got[1] != "talk" {
|
||||
if code != 200 || len(got) != 2 || got[0] != "guide" || got[1] != "help" {
|
||||
t.Fatalf("seed: %d %v", code, got)
|
||||
}
|
||||
code, got = call(e, http.MethodPut, `["talk","studio"]`)
|
||||
if code != 200 || got[0] != "talk" {
|
||||
code, got = call(e, http.MethodPut, `["help","guide"]`)
|
||||
if code != 200 || got[0] != "help" {
|
||||
t.Fatalf("put: %d %v", code, got)
|
||||
}
|
||||
_, got = call(e, http.MethodGet, "")
|
||||
if len(got) != 2 || got[0] != "talk" {
|
||||
if len(got) != 2 || got[0] != "help" {
|
||||
t.Fatalf("round trip: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPutDropsIDsTheCallerCannotSee(t *testing.T) {
|
||||
e := pinsApp(t, false)
|
||||
_, got := call(e, http.MethodPut, `["studio","not-installed"]`)
|
||||
if len(got) != 1 || got[0] != "studio" {
|
||||
_, got := call(e, http.MethodPut, `["guide","not-installed"]`)
|
||||
if len(got) != 1 || got[0] != "guide" {
|
||||
t.Fatalf("an unknown id must be dropped without a hint: %v", got)
|
||||
}
|
||||
}
|
||||
@@ -89,9 +101,19 @@ func TestPutRejectsAListOverTheHardLimit(t *testing.T) {
|
||||
e := pinsApp(t, false)
|
||||
items := make([]string, pins.HardLimit+1)
|
||||
for i := range items {
|
||||
items[i] = `"studio"`
|
||||
items[i] = `"guide"`
|
||||
}
|
||||
if code, _ := call(e, http.MethodPut, "["+strings.Join(items, ",")+"]"); code != 400 {
|
||||
t.Fatalf("want 400, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStudioAndTalkAreNotVisiblePinsWithoutTheAddOns(t *testing.T) {
|
||||
// Studio and Talk are packaged add-ons: the routes list them only for a
|
||||
// caller whose owner lists them as installed, never as built-in pages.
|
||||
e := pinsApp(t, false)
|
||||
_, got := call(e, http.MethodPut, `["studio","talk","guide"]`)
|
||||
if len(got) != 1 || got[0] != "guide" {
|
||||
t.Fatalf("studio and talk are not built in, so they are dropped: %v", got)
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
199
|
||||
182
|
||||
@@ -13,7 +13,6 @@
|
||||
"account": "Konto",
|
||||
"accountFor": "Konto: {{name}}",
|
||||
"sections": {
|
||||
"create": "Erstellen",
|
||||
"recognition": "Erkennung",
|
||||
"build": "Entwickeln",
|
||||
"operate": "Betrieb",
|
||||
@@ -33,8 +32,6 @@
|
||||
"home": "Start",
|
||||
"models": "Models",
|
||||
"chat": "Chat",
|
||||
"studio": "Studio",
|
||||
"talk": "Sprechen",
|
||||
"fineTune": "Fine-Tuning (Experimentell)",
|
||||
"quantize": "Quantisierung (Experimentell)",
|
||||
"faces": "Gesichter",
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"account": "Account",
|
||||
"accountFor": "Account: {{name}}",
|
||||
"sections": {
|
||||
"create": "Create",
|
||||
"recognition": "Recognition",
|
||||
"build": "Build",
|
||||
"operate": "Operate",
|
||||
@@ -34,8 +33,6 @@
|
||||
"home": "Home",
|
||||
"models": "Models",
|
||||
"chat": "Chat",
|
||||
"studio": "Studio",
|
||||
"talk": "Talk",
|
||||
"fineTune": "Fine-Tune",
|
||||
"quantize": "Quantize",
|
||||
"faces": "Faces",
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"account": "Cuenta",
|
||||
"accountFor": "Cuenta: {{name}}",
|
||||
"sections": {
|
||||
"create": "Crear",
|
||||
"recognition": "Reconocimiento",
|
||||
"build": "Construir",
|
||||
"operate": "Operar",
|
||||
@@ -33,8 +32,6 @@
|
||||
"home": "Inicio",
|
||||
"models": "Models",
|
||||
"chat": "Chat",
|
||||
"studio": "Studio",
|
||||
"talk": "Hablar",
|
||||
"fineTune": "Ajuste fino (Experimental)",
|
||||
"quantize": "Cuantización (Experimental)",
|
||||
"faces": "Rostros",
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"account": "Akun",
|
||||
"accountFor": "Akun: {{name}}",
|
||||
"sections": {
|
||||
"create": "Buat",
|
||||
"recognition": "Pengenalan",
|
||||
"build": "Bangun",
|
||||
"operate": "Operasikan",
|
||||
@@ -33,8 +32,6 @@
|
||||
"home": "Beranda",
|
||||
"models": "Models",
|
||||
"chat": "Obrolan",
|
||||
"studio": "Studio",
|
||||
"talk": "Bicara",
|
||||
"fineTune": "Fine-Tune (Eksperimental)",
|
||||
"quantize": "Kuantisasi (Eksperimental)",
|
||||
"faces": "Wajah",
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"account": "Account",
|
||||
"accountFor": "Account: {{name}}",
|
||||
"sections": {
|
||||
"create": "Crea",
|
||||
"recognition": "Riconoscimento",
|
||||
"build": "Costruisci",
|
||||
"operate": "Gestione",
|
||||
@@ -33,8 +32,6 @@
|
||||
"home": "Home",
|
||||
"models": "Models",
|
||||
"chat": "Chat",
|
||||
"studio": "Studio",
|
||||
"talk": "Conversazione",
|
||||
"fineTune": "Fine-Tuning (Sperimentale)",
|
||||
"quantize": "Quantizzazione (Sperimentale)",
|
||||
"faces": "Volti",
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"account": "계정",
|
||||
"accountFor": "계정: {{name}}",
|
||||
"sections": {
|
||||
"create": "만들기",
|
||||
"recognition": "인식",
|
||||
"build": "구축",
|
||||
"operate": "운영",
|
||||
@@ -33,8 +32,6 @@
|
||||
"home": "홈",
|
||||
"models": "Models",
|
||||
"chat": "채팅",
|
||||
"studio": "스튜디오",
|
||||
"talk": "대화",
|
||||
"fineTune": "파인튜닝 (실험적)",
|
||||
"quantize": "양자화 (실험적)",
|
||||
"faces": "얼굴",
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"account": "Conta",
|
||||
"accountFor": "Conta: {{name}}",
|
||||
"sections": {
|
||||
"create": "Criar",
|
||||
"recognition": "Reconhecimento",
|
||||
"build": "Construir",
|
||||
"operate": "Operar",
|
||||
@@ -33,8 +32,6 @@
|
||||
"home": "Início",
|
||||
"models": "Models",
|
||||
"chat": "Chat",
|
||||
"studio": "Studio",
|
||||
"talk": "Falar",
|
||||
"fineTune": "Ajuste Fino",
|
||||
"quantize": "Quantizar",
|
||||
"faces": "Rostos",
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"account": "账户",
|
||||
"accountFor": "账户:{{name}}",
|
||||
"sections": {
|
||||
"create": "创建",
|
||||
"recognition": "识别",
|
||||
"build": "构建",
|
||||
"operate": "运维",
|
||||
@@ -33,8 +32,6 @@
|
||||
"home": "首页",
|
||||
"models": "Models",
|
||||
"chat": "聊天",
|
||||
"studio": "工作室",
|
||||
"talk": "通话",
|
||||
"fineTune": "微调(实验性)",
|
||||
"quantize": "量化(实验性)",
|
||||
"faces": "人脸",
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useAddonRegistry } from './useAddonRegistry'
|
||||
import { appStatus } from './registryState'
|
||||
|
||||
// The address of a page of an installed add-on, or null while the add-on is
|
||||
// absent, disabled, failed or unusable. A caller that links to an add-on page
|
||||
// (a command, a button) shows the link only when this answers an address, so a
|
||||
// person who has not installed the add-on never sees a dead end.
|
||||
export function useAddonPage(addonId, page) {
|
||||
const { apps, acquire, allowed } = useAddonRegistry()
|
||||
// The list is read only for a user the server lets see add-ons.
|
||||
useEffect(() => (allowed ? acquire() : undefined), [acquire, allowed])
|
||||
const app = apps.find(a => a.id === addonId)
|
||||
const ready = appStatus(app) === 'ready' && Array.isArray(app.ui.pages) && app.ui.pages.includes(page)
|
||||
return ready ? `/app/addons/${encodeURIComponent(addonId)}/${encodeURIComponent(page)}` : null
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { useBranding } from '../contexts/BrandingContext'
|
||||
import { apiUrl } from '../utils/basePath'
|
||||
import { preloadRoute } from '../router'
|
||||
import { hubs, hubEntryPath, hubOwnsPath } from './hub/hubConfig'
|
||||
import { topDestinations, createDestinations } from '../utils/destinations'
|
||||
import { topDestinations } from '../utils/destinations'
|
||||
import { primaryDestinations, visiblePrimary } from '../utils/navigationModel'
|
||||
import { useNavigation } from '../context/NavigationContext'
|
||||
import PinnedNav from './PinnedNav'
|
||||
@@ -21,18 +21,13 @@ const SECTIONS_KEY = 'localai_sidebar_sections'
|
||||
// The sidebar has two layouts. The four-item layout shows Home, Chat, Models
|
||||
// and Add-ons, the Pinned section, and Build and Operate under
|
||||
// Workspace. With the legacy switch on (features.addon_navigation === false)
|
||||
// it shows Home and Models, the Create group (Chat, Studio, Talk) and
|
||||
// Workspace. Build and Operate are single entries; each opens a hub whose tab
|
||||
// bar lives in hub/hubConfig.js (shared with HubLayout). The legacy entries
|
||||
// are shared with the 404 page, which lists the same destinations.
|
||||
// it shows Home, Models and Chat, and Workspace. Build and Operate are single
|
||||
// entries; each opens a hub whose tab bar lives in hub/hubConfig.js (shared
|
||||
// with HubLayout). The legacy entries are shared with the 404 page, which
|
||||
// lists the same destinations.
|
||||
const topItems = topDestinations
|
||||
|
||||
const sections = [
|
||||
{
|
||||
id: 'create',
|
||||
titleKey: 'sections.create',
|
||||
items: createDestinations,
|
||||
},
|
||||
// Items come from the hubs (hubConfig.js) and carry their own gating.
|
||||
{ id: 'workspace', titleKey: 'sections.workspace', hubs: true },
|
||||
]
|
||||
@@ -180,8 +175,8 @@ export default function Sidebar({ isOpen, onClose }) {
|
||||
const failedOps = operations.filter((op) => op.error).length
|
||||
const activeOps = operations.length
|
||||
|
||||
// Create carries no gating beyond filterItem. Workspace lists one entry per
|
||||
// hub the viewer can use; its target is the hub's overview.
|
||||
// Workspace lists one entry per hub the viewer can use; its target is the
|
||||
// hub's overview.
|
||||
const getVisibleSectionItems = (section) => {
|
||||
if (!section.hubs) return section.items.filter(filterItem)
|
||||
return hubs.flatMap(hub => {
|
||||
|
||||
+5
-3
@@ -1,7 +1,8 @@
|
||||
// The slash actions on the Home command bar. Every entry has a destination in
|
||||
// the product today; the page decides what each one does (navigate, open the
|
||||
// model list, start a chat). `needs` hides an action the person cannot use:
|
||||
// the admin pages, the assistant, or stopping a model when none is loaded.
|
||||
// the admin pages, the assistant, stopping a model when none is loaded, or
|
||||
// Studio while that add-on is not installed.
|
||||
|
||||
export const SLASH_GROUPS = ['chat', 'models', 'go']
|
||||
|
||||
@@ -14,18 +15,19 @@ export const SLASH_ACTIONS = [
|
||||
{ id: 'installed', cmd: '/installed', icon: 'boxes', group: 'models', needs: 'admin' },
|
||||
{ id: 'import', cmd: '/import', icon: 'upload', group: 'models', needs: 'admin' },
|
||||
{ id: 'stop', cmd: '/stop', icon: 'stop', group: 'models', needs: 'loaded' },
|
||||
{ id: 'studio', cmd: '/studio', icon: 'palette', group: 'go' },
|
||||
{ id: 'studio', cmd: '/studio', icon: 'palette', group: 'go', needs: 'studio' },
|
||||
{ id: 'settings', cmd: '/settings', icon: 'settings', group: 'go', needs: 'admin' },
|
||||
{ id: 'docs', cmd: '/docs', icon: 'book', group: 'go' },
|
||||
]
|
||||
|
||||
// `ctx`: { isAdmin, assistantAvailable, hasModels, loadedCount }
|
||||
// `ctx`: { isAdmin, assistantAvailable, hasModels, loadedCount, studioInstalled }
|
||||
export function availableActions(ctx) {
|
||||
return SLASH_ACTIONS.filter(a => {
|
||||
if (a.needs === 'admin') return ctx.isAdmin
|
||||
if (a.needs === 'assistant') return ctx.isAdmin && ctx.assistantAvailable
|
||||
if (a.needs === 'models') return ctx.hasModels
|
||||
if (a.needs === 'loaded') return ctx.isAdmin && ctx.loadedCount > 0
|
||||
if (a.needs === 'studio') return ctx.studioInstalled === true
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user