Skip to content

Commit b38739c

Browse files
committed
Add TextToSpeech UI
1 parent e88abbc commit b38739c

File tree

2 files changed

+290
-3
lines changed

2 files changed

+290
-3
lines changed

AiServer.ServiceInterface/GenerationServices.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ public static async Task<object> ProcessGeneration(this CreateGeneration diffReq
245245

246246
// Handle failed jobs
247247
if (queuedJob.Failed != null)
248-
throw new Exception($"Job failed: {queuedJob.Failed.Error}");
248+
throw new Exception($"Job failed: {queuedJob.Failed.Error?.Message}");
249249

250250
// Handle cancelled jobs
251251
if (queuedJob.Job?.State == BackgroundJobState.Cancelled)
Lines changed: 289 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,295 @@
1+
import { ref, computed, onMounted, inject, watch, nextTick } from "vue"
2+
import { useClient, useFiles } from "@servicestack/vue"
3+
import { createErrorStatus } from "@servicestack/client"
4+
import { TextToSpeech } from "dtos"
5+
import { UiLayout, ThreadStorage, HistoryTitle, HistoryGroups, useUiLayout, icons, toArtifacts, acceptedImages } from "../utils.mjs"
6+
import { ArtifactGallery } from "./Artifacts.mjs"
7+
import FileUpload from "./FileUpload.mjs"
8+
19
export default {
10+
components: {
11+
UiLayout,
12+
HistoryTitle,
13+
HistoryGroups,
14+
ArtifactGallery,
15+
FileUpload,
16+
},
217
template: `
3-
<h3 class="p-2 sm:block text-xl md:text-2xl font-semibold">Text to Speech</h3>
18+
<UiLayout>
19+
<template #main>
20+
<div class="flex flex-1 gap-4 text-base md:gap-5 lg:gap-6">
21+
<form ref="refForm" :disabled="client.loading.value" class="w-full mb-0" @submit.prevent="send">
22+
<div class="relative flex h-full max-w-full flex-1 flex-col">
23+
<div class="flex flex-col w-full items-center">
24+
<fieldset class="w-full">
25+
<ErrorSummary :except="visibleFields" class="mb-4" />
26+
<div class="grid grid-cols-6 gap-4">
27+
<div class="col-span-6">
28+
<TextareaInput inputClass="h-48" id="text" v-model="request.text" />
29+
</div>
30+
<div class="col-span-6 sm:col-span-3">
31+
<TextInput type="number" id="seed" v-model="request.seed" min="0" />
32+
</div>
33+
<div class="col-span-6 sm:col-span-3">
34+
<TextInput id="tag" v-model="request.tag" placeholder="Tag" />
35+
</div>
36+
</div>
37+
</fieldset>
38+
</div>
39+
<div class="mt-4 mb-8 flex justify-center">
40+
<PrimaryButton :key="renderKey" type="submit" :disabled="!validPrompt()">
41+
<span class="text-base font-semibold">Submit</span>
42+
</PrimaryButton>
43+
</div>
44+
</div>
45+
</form>
46+
</div>
47+
48+
<div class="pb-20">
49+
50+
<div v-if="client.loading.value" class="mt-8 mb-20 flex justify-center items-center">
51+
<Loading class="text-gray-300 font-normal" imageClass="w-7 h-7 mt-1.5">processing image...</Loading>
52+
</div>
53+
54+
<div v-for="result in getThreadResults()" class="w-full ">
55+
<div class="flex items-center justify-between">
56+
<span class="my-4 flex justify-center items-center text-xl underline-offset-4">
57+
<span>{{ result.request.image }}</span>
58+
</span>
59+
<div class="group flex cursor-pointer" @click="discardResult(result)">
60+
<div class="ml-1 invisible group-hover:visible">discard</div>
61+
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="currentColor" d="M12 12h2v12h-2zm6 0h2v12h-2z"></path><path fill="currentColor" d="M4 6v2h2v20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8h2V6zm4 22V8h16v20zm4-26h8v2h-8z"></path></svg>
62+
</div>
63+
</div>
64+
65+
<ArtifactGallery :results="toArtifacts(result)">
66+
<template #bottom="{ selected }">
67+
<div class="z-40 fixed bottom-0 gap-x-6 w-full flex justify-center p-4 bg-black/20">
68+
<a :href="selected.url + '?download=1'" class="flex text-sm text-gray-300 hover:text-gray-100 hover:drop-shadow">
69+
<svg class="w-5 h-5 mr-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 20h12M12 4v12m0 0l3.5-3.5M12 16l-3.5-3.5"></path></svg>
70+
download
71+
</a>
72+
<div @click.stop.prevent="toggleIcon(selected)" class="flex cursor-pointer text-sm text-gray-300 hover:text-gray-100 hover:drop-shadow">
73+
<svg :class="['w-5 h-5 mr-0.5',selected.url == threadRef.icon ? '-rotate-45' : '']" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m14 18l-8 8M20.667 4L28 11.333l-6.38 6.076a2 2 0 0 0-.62 1.448v3.729c0 .89-1.077 1.337-1.707.707L8.707 12.707c-.63-.63-.184-1.707.707-1.707h3.729a2 2 0 0 0 1.448-.62z"/></svg>
74+
{{selected.url == threadRef.icon ? 'unpin icon' : 'pin icon' }}
75+
</div>
76+
</div>
77+
</template>
78+
</ArtifactGallery>
79+
</div>
80+
</div>
81+
</template>
82+
83+
<template #sidebar>
84+
<HistoryTitle :prefix="storage.prefix" />
85+
<HistoryGroups :history="history" v-slot="{ item }" @save="saveHistoryItem($event)" @remove="removeHistoryItem($event)">
86+
<Icon class="h-5 w-5 rounded-full flex-shrink-0 mr-1" :src="item.icon ?? icons.image" loading="lazy" :alt="item.model" />
87+
<span :title="item.title">{{item.title}}</span>
88+
</HistoryGroups>
89+
</template>
90+
</UiLayout>
491
`,
592
setup() {
6-
return {}
93+
const client = useClient()
94+
const routes = inject('routes')
95+
const refUi = ref()
96+
const refForm = ref()
97+
const refImage = ref()
98+
const ui = useUiLayout(refUi)
99+
const renderKey = ref(0)
100+
101+
const storage = new ThreadStorage(`txt2spch`, {
102+
text: '',
103+
tag: '',
104+
seed: '',
105+
})
106+
const error = ref()
107+
108+
const prefs = ref(storage.getPrefs())
109+
const history = ref(storage.getHistory())
110+
const thread = ref()
111+
const threadRef = ref()
112+
113+
const validPrompt = () => !!request.value.text
114+
const refMessage = ref()
115+
const visibleFields = 'text'.split(',')
116+
const request = ref(new TextToSpeech())
117+
const activeModels = ref([])
118+
119+
function savePrefs() {
120+
storage.savePrefs(Object.assign({}, request.value, { tag:'' }))
121+
}
122+
function loadHistory() {
123+
history.value = storage.getHistory()
124+
}
125+
function saveHistory() {
126+
storage.saveHistory(history.value)
127+
}
128+
function saveThread() {
129+
if (thread.value) {
130+
storage.saveThread(thread.value)
131+
}
132+
}
133+
134+
async function send() {
135+
console.log('send', validPrompt(), client.loading.value)
136+
if (!validPrompt() || client.loading.value) return
137+
138+
savePrefs()
139+
console.debug(`${storage.prefix}.request`, Object.assign({}, request.value))
140+
141+
error.value = null
142+
143+
const api = await client.api(request.value, { jsconfig: 'eccn' })
144+
/** @type {GenerationResponse} */
145+
const r = api.response
146+
if (r) {
147+
console.debug(`${storage.prefix}.response`, r)
148+
149+
if (!r.outputs?.length) {
150+
error.value = createErrorStatus("no results were returned")
151+
} else {
152+
const id = parseInt(routes.id) || storage.createId()
153+
thread.value = thread.value ?? storage.createThread(Object.assign({
154+
id: storage.getThreadId(id),
155+
title: request.text,
156+
}, request.value))
157+
158+
const result = {
159+
id: storage.createId(),
160+
request: Object.assign({}, request.value),
161+
response: r,
162+
}
163+
thread.value.results.push(result)
164+
saveThread()
165+
166+
if (!history.value.find(x => x.id === id)) {
167+
history.value.push({
168+
id,
169+
title: thread.value.title,
170+
icon: r.outputs[0].url
171+
})
172+
}
173+
saveHistory()
174+
175+
if (routes.id !== id) {
176+
routes.to({ id })
177+
}
178+
}
179+
180+
} else {
181+
console.error('send.error', api.error)
182+
error.value = api.error
183+
}
184+
}
185+
186+
function getThreadResults() {
187+
const ret = Array.from(thread.value?.results ?? [])
188+
ret.reverse()
189+
return ret
190+
}
191+
192+
function selectRequest(req) {
193+
Object.assign(request.value, req)
194+
ui.scrollTop()
195+
}
196+
197+
function discardResult(result) {
198+
thread.value.results = thread.value.results.filter(x => x.id != result.id)
199+
saveThread()
200+
}
201+
202+
function toggleIcon(item) {
203+
threadRef.value.icon = item.url
204+
saveHistory()
205+
}
206+
207+
function onRouteChange() {
208+
console.log('onRouteChange', routes.id)
209+
refImage.value?.clear()
210+
loadHistory()
211+
if (routes.id) {
212+
const id = parseInt(routes.id)
213+
thread.value = storage.getThread(storage.getThreadId(id))
214+
threadRef.value = history.value.find(x => x.id === parseInt(routes.id))
215+
console.debug('thread', thread.value, threadRef.value)
216+
if (thread.value) {
217+
Object.keys(storage.defaults).forEach(k =>
218+
request.value[k] = thread.value[k] ?? storage.defaults[k])
219+
}
220+
} else {
221+
thread.value = null
222+
Object.keys(storage.defaults).forEach(k => request.value[k] = storage.defaults[k])
223+
}
224+
}
225+
226+
function updated() {
227+
onRouteChange()
228+
}
229+
230+
function saveHistoryItem(item) {
231+
storage.saveHistory(history.value)
232+
if (thread.value && item.title) {
233+
thread.value.title = item.title
234+
saveThread()
235+
}
236+
}
237+
238+
function removeHistoryItem(item) {
239+
const idx = history.value.findIndex(x => x.id === item.id)
240+
if (idx >= 0) {
241+
history.value.splice(idx, 1)
242+
storage.saveHistory(history.value)
243+
storage.deleteThread(storage.getThreadId(item.id))
244+
if (routes.id == item.id) {
245+
routes.to({ id:undefined })
246+
}
247+
}
248+
}
249+
250+
251+
watch(() => routes.id, updated)
252+
watch(() => [
253+
request.value.seed,
254+
request.value.tag,
255+
], () => {
256+
if (!thread.value) return
257+
Object.keys(storage.defaults).forEach(k =>
258+
thread.value[k] = request.value[k] ?? storage.defaults[k])
259+
saveThread()
260+
})
261+
262+
onMounted(async () => {
263+
updated()
264+
})
265+
266+
return {
267+
refForm,
268+
refImage,
269+
storage,
270+
routes,
271+
client,
272+
history,
273+
request,
274+
visibleFields,
275+
validPrompt,
276+
refMessage,
277+
activeModels,
278+
thread,
279+
threadRef,
280+
icons,
281+
send,
282+
saveHistory,
283+
saveThread,
284+
toggleIcon,
285+
selectRequest,
286+
discardResult,
287+
getThreadResults,
288+
saveHistoryItem,
289+
removeHistoryItem,
290+
toArtifacts,
291+
acceptedImages,
292+
renderKey,
293+
}
7294
}
8295
}

0 commit comments

Comments
 (0)