Merge branch 'kwaroran:main' into main
This commit is contained in:
@@ -7,6 +7,8 @@
|
||||
import { MessageSquareIcon, PlusIcon } from "lucide-svelte";
|
||||
|
||||
const agoFormatter = new Intl.RelativeTimeFormat(navigator.languages, { style: 'short' });
|
||||
|
||||
let {gridMode = false, endGrid = () => {}} = $props();
|
||||
|
||||
function makeAgoText(time:number){
|
||||
if(time === 0){
|
||||
@@ -60,6 +62,7 @@
|
||||
{#if char.name.toLocaleLowerCase().includes($MobileSearch.toLocaleLowerCase())}
|
||||
<button class="flex p-2 border-t-darkborderc gap-2 w-full" class:border-t={i !== 0} onclick={() => {
|
||||
changeChar(char.i)
|
||||
endGrid()
|
||||
}}>
|
||||
<BarIcon additionalStyle={getCharImage(char.image, 'css')}></BarIcon>
|
||||
<div class="flex flex-1 w-full flex-col justify-start items-start text-start">
|
||||
@@ -76,8 +79,10 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<button class="p-4 rounded-full absolute bottom-2 right-2 bg-borderc" onclick={() => {
|
||||
addCharacter()
|
||||
}}>
|
||||
<PlusIcon size={24} />
|
||||
</button>
|
||||
{#if gridMode}
|
||||
<button class="p-4 rounded-full absolute bottom-2 right-2 bg-borderc" onclick={() => {
|
||||
addCharacter()
|
||||
}}>
|
||||
<PlusIcon size={24} />
|
||||
</button>
|
||||
{/if}
|
||||
@@ -10,13 +10,14 @@
|
||||
import { language } from "src/lang";
|
||||
import { parseMultilangString } from "src/ts/util";
|
||||
import { checkCharOrder } from "src/ts/globalApi.svelte";
|
||||
import MobileCharacters from "../Mobile/MobileCharacters.svelte";
|
||||
interface Props {
|
||||
endGrid?: any;
|
||||
}
|
||||
|
||||
let { endGrid = () => {} }: Props = $props();
|
||||
let search = $state('')
|
||||
let selected = $state(0)
|
||||
let selected = $state(3)
|
||||
|
||||
function changeChar(index = -1){
|
||||
characterFormatUpdate(index)
|
||||
@@ -57,22 +58,19 @@
|
||||
|
||||
<div class="h-full w-full flex justify-center">
|
||||
<div class="h-full p-6 bg-darkbg max-w-full w-2xl flex flex-col overflow-y-auto">
|
||||
<h1 class="text-textcolor text-2xl font-bold mt-2 flex items-center mx-4 mb-2">
|
||||
<button class="mr-2 hover:text-textcolor text-textcolor2" onclick={() => {
|
||||
endGrid()
|
||||
}}><ArrowLeft /></button>
|
||||
<span>Catalog</span>
|
||||
</h1>
|
||||
<div class="mx-4 mb-6 flex flex-col">
|
||||
<TextInput placeholder="Search" bind:value={search} size="lg" autocomplete="off"/>
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
<Button selected={selected === 0} size="sm" onclick={() => {selected = 0}}>
|
||||
<Button styled={selected === 3 ? 'primary' : 'outlined'} size="sm" onclick={() => {selected = 3}}>
|
||||
{language.simple}
|
||||
</Button>
|
||||
<Button styled={selected === 0 ? 'primary' : 'outlined'} size="sm" onclick={() => {selected = 0}}>
|
||||
{language.grid}
|
||||
</Button>
|
||||
<Button selected={selected === 1} size="sm" onclick={() => {selected = 1}}>
|
||||
<Button styled={selected === 1 ? 'primary' : 'outlined'} size="sm" onclick={() => {selected = 1}}>
|
||||
{language.list}
|
||||
</Button>
|
||||
<Button selected={selected === 2} size="sm" onclick={() => {selected = 2}}>
|
||||
<Button styled={selected === 2 ? 'primary' : 'outlined'} size="sm" onclick={() => {selected = 2}}>
|
||||
{language.trash}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -143,6 +141,8 @@
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{:else if selected === 3}
|
||||
<MobileCharacters gridMode endGrid={endGrid} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,34 +1,44 @@
|
||||
<div class="max-w-sm mx-auto overflow-hidden bg-white rounded-lg shadow-lg hover:shadow-xl transition-shadow duration-300 ease-in-out">
|
||||
<div class="relative">
|
||||
<!-- Image -->
|
||||
<img class="w-full h-48 object-cover transform hover:scale-105 transition-transform duration-300 ease-in-out"
|
||||
src="your-image-url.jpg"
|
||||
alt="Card">
|
||||
<!-- Optional overlay gradient -->
|
||||
<div class="absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-black/60 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<!-- Title -->
|
||||
<h2 class="mb-3 text-2xl font-bold text-gray-800 hover:text-indigo-600 transition-colors duration-300">
|
||||
Card Title
|
||||
</h2>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="text-gray-600 leading-relaxed mb-4">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptates rerum quisquam, temporibus quasi distinctio magnam.
|
||||
</p>
|
||||
|
||||
<!-- Optional footer with button -->
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<button class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors duration-300 transform hover:scale-105">
|
||||
Learn More
|
||||
</button>
|
||||
|
||||
<!-- Optional metadata -->
|
||||
<span class="text-sm text-gray-500">
|
||||
5 min read
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script lang="ts">
|
||||
import { language } from "src/lang";
|
||||
import TextAreaInput from "../UI/GUI/TextAreaInput.svelte";
|
||||
import Button from "../UI/GUI/Button.svelte";
|
||||
import { generateAIImage } from "src/ts/process/stableDiff";
|
||||
import { createBlankChar } from "src/ts/characters";
|
||||
let prompt = $state("");
|
||||
let negPrompt = $state("");
|
||||
let img = $state("");
|
||||
let generating = $state(false)
|
||||
const run = async () => {
|
||||
console.log('running')
|
||||
if(generating){
|
||||
return
|
||||
}
|
||||
generating = true
|
||||
const gen = await generateAIImage(prompt, createBlankChar(), negPrompt, 'inlay')
|
||||
generating = false
|
||||
if(gen){
|
||||
img = gen
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<h2 class="text-4xl text-textcolor my-6 font-black relative">{language.imageGeneration}</h2>
|
||||
|
||||
<span class="text-textcolor text-lg">Prompt</span>
|
||||
<TextAreaInput bind:value={prompt} />
|
||||
|
||||
<span class="text-textcolor text-lg">Neg. Prompt</span>
|
||||
<TextAreaInput bind:value={negPrompt} />
|
||||
|
||||
{#if img}
|
||||
<span class="text-textcolor text-lg">Generated</span>
|
||||
<img src={img} class="max-w-full mt-4" alt="Generated"/>
|
||||
{/if}
|
||||
|
||||
<Button className="mt-6" onclick={run}>
|
||||
{#if generating}
|
||||
<div class="loadmove"></div>
|
||||
{:else}
|
||||
Generate
|
||||
{/if}
|
||||
</Button>
|
||||
15
src/lib/Playground/PlaygroundImageTrans.svelte
Normal file
15
src/lib/Playground/PlaygroundImageTrans.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { language } from "src/lang";
|
||||
import TextInput from "../UI/GUI/TextInput.svelte";
|
||||
import TextAreaInput from "../UI/GUI/TextAreaInput.svelte";
|
||||
|
||||
let selLang = $state("en");
|
||||
let prompt = $state("");
|
||||
</script>
|
||||
|
||||
|
||||
<span class="text-textcolor text-lg mt-4">{language.destinationLanguage}</span>
|
||||
<TextInput bind:value={selLang} />
|
||||
|
||||
<span class="text-textcolor text-lg mt-4">{language.prompt}</span>
|
||||
<TextAreaInput bind:value={prompt} />
|
||||
@@ -14,6 +14,7 @@
|
||||
import PlaygroundParser from "./PlaygroundParser.svelte";
|
||||
import ToolConvertion from "./ToolConvertion.svelte";
|
||||
import { joinMultiuserRoom } from "src/ts/sync/multiuser";
|
||||
import PlaygroundSubtitle from "./PlaygroundSubtitle.svelte";
|
||||
|
||||
let easterEggTouch = $state(0)
|
||||
|
||||
@@ -83,6 +84,11 @@
|
||||
}}>
|
||||
<h1 class="text-2xl font-bold text-start">Parser</h1>
|
||||
</button>
|
||||
<button class="bg-darkbg rounded-md p-6 flex flex-col transition-shadow hover:ring-1" onclick={() => {
|
||||
PlaygroundStore.set(9)
|
||||
}}>
|
||||
<h1 class="text-2xl font-bold text-start">{language.subtitles}</h1>
|
||||
</button>
|
||||
<button class="bg-darkbg rounded-md p-6 flex flex-col transition-shadow hover:ring-1" onclick={() => {
|
||||
PlaygroundStore.set(101)
|
||||
}}>
|
||||
@@ -139,6 +145,9 @@
|
||||
{#if $PlaygroundStore === 8}
|
||||
<PlaygroundParser/>
|
||||
{/if}
|
||||
{#if $PlaygroundStore === 9}
|
||||
<PlaygroundSubtitle/>
|
||||
{/if}
|
||||
{#if $PlaygroundStore === 101}
|
||||
<ToolConvertion/>
|
||||
{/if}
|
||||
|
||||
565
src/lib/Playground/PlaygroundSubtitle.svelte
Normal file
565
src/lib/Playground/PlaygroundSubtitle.svelte
Normal file
@@ -0,0 +1,565 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { language } from "src/lang";
|
||||
import TextInput from "../UI/GUI/TextInput.svelte";
|
||||
import TextAreaInput from "../UI/GUI/TextAreaInput.svelte";
|
||||
import Button from "../UI/GUI/Button.svelte";
|
||||
import { DBState } from "src/ts/stores.svelte";
|
||||
import { getModelInfo, LLMFlags } from "src/ts/model/modellist";
|
||||
import { requestChatData } from "src/ts/process/request";
|
||||
import { selectFileByDom, selectSingleFile, sleep } from "src/ts/util";
|
||||
import { alertError, alertSelect } from "src/ts/alert";
|
||||
import { risuChatParser } from "src/ts/parser.svelte";
|
||||
import { AppendableBuffer, downloadFile, globalFetch } from "src/ts/globalApi.svelte";
|
||||
import SliderInput from "../UI/GUI/SliderInput.svelte";
|
||||
import SelectInput from "../UI/GUI/SelectInput.svelte";
|
||||
import OptionInput from "../UI/GUI/OptionInput.svelte";
|
||||
import sendSound from '../../etc/send.mp3'
|
||||
|
||||
|
||||
|
||||
let LLMModePrompt ="Transcribe and create a caption and timestamp of it, according to the user's audio or video input. inside a markdown code block. (prefix ```webvtt / postfix ```)\n\nFormat\n```\n[TIME] CONTENT\n```\n\nExample\n```\n[00:00] Hildy!\n[00:01] How are you?\n[00:03] Tell me, is the lord of the universe in?\n[00:07] Somebody must've stolen the crown jewels\n```\n\nStep 2. Generate another subtitle, this time, as a translation to {{slot}}, with same format with Step 1., using step 1 as ref.\n\n The translation must be in natural {{slot}}.\n\n Now, start (Hint: media length is {{slot::time}})"
|
||||
let WhisperModePrompt = "```\n{{slot::data}}\n``` Translate the following WEBVTT to natural {{slot}}, with keeping the timestamp and header, inside a markdown code block. (prefix ``` / postfix ```)"
|
||||
|
||||
let selLang = $state(DBState.db.language)
|
||||
let prompt = $state(LLMModePrompt)
|
||||
let modelInfo = $derived(getModelInfo(DBState.db.aiModel))
|
||||
let outputText = $state('')
|
||||
let fileB64 = $state('')
|
||||
let vttB64 = $state('')
|
||||
let vobj:TranscribeObj[] = $state([])
|
||||
let mode = $state('llm')
|
||||
let sourceLang:string|null = $state(null)
|
||||
|
||||
function getLanguageCodes(){
|
||||
let languageCodes:{
|
||||
code: string
|
||||
name: string
|
||||
}[] = []
|
||||
|
||||
for(let i=0x41;i<=0x5A;i++){
|
||||
for(let j=0x41;j<=0x5A;j++){
|
||||
languageCodes.push({
|
||||
code: String.fromCharCode(i) + String.fromCharCode(j),
|
||||
name: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
languageCodes = languageCodes.map(v => {
|
||||
return {
|
||||
code: v.code,
|
||||
name: new Intl.DisplayNames([
|
||||
DBState.db.language === 'cn' ? 'zh' : DBState.db.language
|
||||
], {
|
||||
type: 'language',
|
||||
fallback: 'none'
|
||||
}).of(v.code)
|
||||
}
|
||||
}).filter((a) => {
|
||||
return a.name
|
||||
}).sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
return languageCodes
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function runLLMMode() {
|
||||
outputText = 'Loading...\n\n'
|
||||
|
||||
const file = await selectSingleFile([
|
||||
'mp3', 'ogg', 'wav', 'flac',
|
||||
'mp4', 'webm', 'mkv', 'avi', 'mov'
|
||||
])
|
||||
|
||||
if(!file){
|
||||
outputText = ''
|
||||
return
|
||||
}
|
||||
|
||||
const videos = [
|
||||
'mp4', 'webm', 'mkv', 'avi', 'mov'
|
||||
]
|
||||
|
||||
const ext = file.name.split('.').pop()
|
||||
|
||||
fileB64 = `data:${
|
||||
videos.includes(ext) ? 'video' : 'audio'
|
||||
}/${ext};base64,${Buffer.from(file.data).toString('base64')}`
|
||||
|
||||
const media = {
|
||||
type: videos.includes(ext) ? 'video' : 'audio',
|
||||
base64: fileB64,
|
||||
} as const
|
||||
|
||||
let time = ''
|
||||
|
||||
if(prompt.includes('{{slot::time}}')){
|
||||
const video = document.createElement('video')
|
||||
video.src = fileB64
|
||||
video.preload = 'metadata'
|
||||
video.muted = true
|
||||
await video.play()
|
||||
const d = video.duration
|
||||
console.log(d)
|
||||
if(isNaN(d)){
|
||||
time = 'unknown'
|
||||
}else{
|
||||
time = `${Math.floor(d / 60)}:${Math.floor(d % 60)}`
|
||||
}
|
||||
video.pause()
|
||||
video.remove()
|
||||
}
|
||||
|
||||
const v =await requestChatData({
|
||||
formated: [{
|
||||
role: "user",
|
||||
content: risuChatParser(prompt).replace(/{{slot}}/g, selLang).replace(/{{slot::time}}/g, time),
|
||||
multimodals: [media]
|
||||
}],
|
||||
bias: {},
|
||||
useStreaming: true
|
||||
}, 'model')
|
||||
|
||||
if(v.type === 'multiline'){
|
||||
alertError(v.result[0][1])
|
||||
return
|
||||
}
|
||||
|
||||
if(v.type !== 'streaming'){
|
||||
alertError(v.result)
|
||||
return
|
||||
}
|
||||
|
||||
const reader = v.result.getReader()
|
||||
|
||||
while(true){
|
||||
const { done, value } = await reader.read()
|
||||
if(done){
|
||||
break
|
||||
}
|
||||
const firstKey = Object.keys(value)[0]
|
||||
|
||||
outputText = value[firstKey]
|
||||
}
|
||||
|
||||
const extracted = outputText.matchAll(/```(web)?(vtt)?\n(.*?)\n```/gs)
|
||||
|
||||
let latest = ''
|
||||
for(const match of extracted){
|
||||
latest = match[3].trim()
|
||||
}
|
||||
|
||||
vobj = convertTransToObj(latest)
|
||||
outputText = makeWebVtt(vobj)
|
||||
vttB64 = `data:text/vtt;base64,${Buffer.from(outputText).toString('base64')}`
|
||||
|
||||
const audio = new Audio(sendSound);
|
||||
audio.play();
|
||||
}
|
||||
|
||||
async function runWhisperMode() {
|
||||
outputText = 'Loading...\n\n'
|
||||
|
||||
const files = await selectFileByDom([
|
||||
'mp3', 'ogg', 'wav', 'flac',
|
||||
'mp4', 'webm', 'mkv', 'avi', 'mov'
|
||||
|
||||
])
|
||||
|
||||
const file = files?.[0]
|
||||
|
||||
let requestFile:File = null
|
||||
|
||||
if(!file){
|
||||
outputText = ''
|
||||
return
|
||||
}
|
||||
const videos = [
|
||||
'mp4', 'webm', 'mkv', 'avi', 'mov'
|
||||
]
|
||||
|
||||
const ext = file.name.split('.').pop()
|
||||
if(videos.includes(ext)){
|
||||
|
||||
|
||||
//check duration
|
||||
let duration = 0
|
||||
{
|
||||
const video = document.createElement('video')
|
||||
video.src = URL.createObjectURL(file)
|
||||
video.preload = 'metadata'
|
||||
video.muted = true
|
||||
await video.play()
|
||||
const d = video.duration
|
||||
if(isNaN(d)){
|
||||
alertError('This video does not have a duration')
|
||||
return
|
||||
}
|
||||
video.pause()
|
||||
video.remove()
|
||||
duration = d
|
||||
}
|
||||
|
||||
outputText = 'Converting video to audio...\n\n'
|
||||
const audioContext = new AudioContext()
|
||||
const audioBuffer = await audioContext.decodeAudioData(await file.arrayBuffer())
|
||||
|
||||
const [left, right] = [audioBuffer.getChannelData(0), audioBuffer.getChannelData(1)]
|
||||
|
||||
const leftInt16 = new Int16Array(left.length)
|
||||
const rightInt16 = new Int16Array(right.length)
|
||||
|
||||
for(let i = 0; i < left.length; i++){
|
||||
leftInt16[i] = left[i] * 0x7FFF
|
||||
rightInt16[i] = right[i] * 0x7FFF
|
||||
}
|
||||
|
||||
const lamejs = await import('@breezystack/lamejs')
|
||||
const mp3encoder = new lamejs.Mp3Encoder(2, 44100, 128);
|
||||
const enc = new AppendableBuffer()
|
||||
|
||||
for(let pointer = 0; pointer < leftInt16.length; pointer += 1152){
|
||||
enc.append(mp3encoder.encodeBuffer(leftInt16.subarray(pointer, pointer + 1152), rightInt16.subarray(pointer, pointer + 1152)))
|
||||
if(pointer % 115200 === 0){
|
||||
outputText = `Converting video to audio... ${(pointer / leftInt16.length * 100).toFixed(2)}%\n`
|
||||
await sleep(1)
|
||||
}
|
||||
}
|
||||
enc.append(mp3encoder.flush())
|
||||
|
||||
const file2 = new File([enc.buffer], 'audio.mp3', {
|
||||
type: 'audio/mp3'
|
||||
})
|
||||
|
||||
outputText = 'Transcribing audio...\n\n'
|
||||
requestFile = file2
|
||||
}
|
||||
else{
|
||||
requestFile = file
|
||||
}
|
||||
|
||||
|
||||
if(mode === 'whisperLocal'){
|
||||
try {
|
||||
const {pipeline} = await import('@huggingface/transformers')
|
||||
let stats:{
|
||||
[key:string]:{
|
||||
name:string
|
||||
status:string
|
||||
file:string
|
||||
progress?:number
|
||||
}
|
||||
} = {}
|
||||
|
||||
const device = ('gpu' in navigator) ? 'webgpu' : 'wasm'
|
||||
|
||||
const transcriber = await pipeline(
|
||||
"automatic-speech-recognition",
|
||||
"onnx-community/whisper-large-v3-turbo_timestamped",
|
||||
{
|
||||
device: device,
|
||||
progress_callback: (progress) => {
|
||||
stats[progress.name + progress.file] = progress
|
||||
outputText = Object.values(stats).map(v => `${v.name}-${v.file}: ${progress.status} ${v.progress ? `[${v.progress.toFixed(2)}%]` : ''}`).join('\n')
|
||||
},
|
||||
dtype: 'q8'
|
||||
},
|
||||
);
|
||||
|
||||
const audioContext = new AudioContext()
|
||||
const audioBuffer = await audioContext.decodeAudioData(await requestFile.arrayBuffer())
|
||||
const combined = new Float32Array(audioBuffer.getChannelData(0).length)
|
||||
for(let j = 0; j < audioBuffer.getChannelData(0).length; j++){
|
||||
for(let i = 0; i < audioBuffer.numberOfChannels; i++){
|
||||
combined[j] += audioBuffer.getChannelData(i)[j]
|
||||
}
|
||||
|
||||
if(combined[j] > 1){
|
||||
combined[j] = 1
|
||||
}
|
||||
if(combined[j] < -1){
|
||||
combined[j] = -1
|
||||
}
|
||||
}
|
||||
|
||||
outputText = ('Transcribing... (This may take a while. Do not close the tab.)')
|
||||
if(device !== 'webgpu'){
|
||||
outputText += `\nYour browser or OS do not support WebGPU, so the transcription may be slower.`
|
||||
}
|
||||
await sleep(10)
|
||||
const res1 = await transcriber(combined, {
|
||||
return_timestamps: true,
|
||||
language: sourceLang,
|
||||
})
|
||||
const res2 = Array.isArray(res1) ? res1[0] : res1
|
||||
const chunks = res2.chunks
|
||||
|
||||
outputText = 'WEBVTT\n\n'
|
||||
|
||||
for(const chunk of chunks){
|
||||
outputText += `${chunk.timestamp[0]} --> ${chunk.timestamp[1]}\n${chunk.text}\n\n`
|
||||
}
|
||||
|
||||
console.log(outputText)
|
||||
|
||||
} catch (error) {
|
||||
alertError(JSON.stringify(error))
|
||||
outputText = ''
|
||||
return
|
||||
}
|
||||
}
|
||||
else{
|
||||
const formData = new FormData()
|
||||
formData.append('file', requestFile)
|
||||
formData.append('model', 'whisper-1')
|
||||
formData.append('response_format', 'vtt')
|
||||
|
||||
|
||||
const d = await fetch('https://api.openai.com/v1/audio/transcriptions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${DBState.db.openAIKey}`
|
||||
},
|
||||
body: formData
|
||||
|
||||
})
|
||||
outputText = await d.text()
|
||||
}
|
||||
|
||||
|
||||
const v = await requestChatData({
|
||||
formated: [{
|
||||
role: "user",
|
||||
content: risuChatParser(prompt).replace(/{{slot}}/g, selLang).replace(/{{slot::data}}/g, outputText),
|
||||
}],
|
||||
bias: {},
|
||||
useStreaming: true
|
||||
}, 'model')
|
||||
|
||||
|
||||
if(v.type === 'multiline'){
|
||||
alertError(v.result[0][1])
|
||||
return
|
||||
}
|
||||
|
||||
if(v.type !== 'streaming'){
|
||||
alertError(v.result)
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Reading...")
|
||||
|
||||
const reader = v.result.getReader()
|
||||
|
||||
while(true){
|
||||
const { done, value } = await reader.read()
|
||||
if(done){
|
||||
break
|
||||
}
|
||||
const firstKey = Object.keys(value)[0]
|
||||
|
||||
outputText = value[firstKey]
|
||||
}
|
||||
if(!outputText.trim().endsWith('```')){
|
||||
outputText = outputText.trim() + '\n```'
|
||||
}
|
||||
|
||||
const extracted = outputText.matchAll(/```(web)?(vtt)?\n(.*?)\n```/gs)
|
||||
|
||||
let latest = ''
|
||||
for(const match of extracted){
|
||||
latest = match[3].trim()
|
||||
}
|
||||
|
||||
const fileBuffer = await file.arrayBuffer()
|
||||
outputText = latest
|
||||
vttB64 = `data:text/vtt;base64,${Buffer.from(outputText).toString('base64')}`
|
||||
fileB64 = `data:audio/wav;base64,${Buffer.from(fileBuffer).toString('base64')}`
|
||||
vobj = convertWebVTTtoObj(outputText)
|
||||
|
||||
const audio = new Audio(sendSound);
|
||||
audio.play();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
type TranscribeObj = {
|
||||
start: string
|
||||
end: string
|
||||
text: string
|
||||
}
|
||||
|
||||
|
||||
function convertTransToObj(r:string){
|
||||
const lines = r.split('\n').map(v => v.trim()).filter(v => v)
|
||||
const obj:TranscribeObj[] = []
|
||||
for(let i = 0; i < lines.length; i++){
|
||||
const line = lines[i]
|
||||
if(line.startsWith('[')){
|
||||
let [time, ...text] = line.split(']')
|
||||
time = time.slice(1)
|
||||
if(obj.length > 0){
|
||||
obj[obj.length - 1].end = time + '.000'
|
||||
}
|
||||
obj.push({
|
||||
start: time + '.000',
|
||||
end: '',
|
||||
text: text.join(' ')
|
||||
})
|
||||
}
|
||||
}
|
||||
//rediculously long line
|
||||
obj[obj.length - 1].end = '99:99.000'
|
||||
return obj
|
||||
}
|
||||
|
||||
function convertWebVTTtoObj(r:string){
|
||||
const chunks = r.split('\n\n').map(v => v.trim()).filter(v => v)
|
||||
const obj:TranscribeObj[] = []
|
||||
for(const chunk of chunks){
|
||||
if(chunk.startsWith('WEBVTT')){
|
||||
continue
|
||||
}
|
||||
const [time, ...text] = chunk.split('\n')
|
||||
const [start, end] = time.split(' --> ')
|
||||
obj.push({
|
||||
start: start,
|
||||
end: end,
|
||||
text: text.join('\n')
|
||||
})
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
function makeWebVtt(obj: TranscribeObj[]){
|
||||
let vtt = 'WEBVTT\n\n'
|
||||
|
||||
for(const line of obj){
|
||||
vtt += `${line.start} --> ${line.end}\n${line.text}\n\n`
|
||||
}
|
||||
|
||||
return vtt
|
||||
}
|
||||
|
||||
function webVttToSrt(){
|
||||
const srt = outputText.replace('WEBVTT', '').trim().split('\n\n').map((v, i) => {
|
||||
const [time, ...text] = v.split('\n')
|
||||
const [start, end] = time.split(' --> ')
|
||||
return `${i + 1}\n${start.replace('.', ',')} --> ${end.replace('.', ',')}\n${text.join('\n')}`
|
||||
})
|
||||
return srt
|
||||
}
|
||||
|
||||
type WaveOptions = {
|
||||
isFloat: boolean
|
||||
numChannels: number
|
||||
sampleRate: number
|
||||
}
|
||||
</script>
|
||||
|
||||
<h2 class="text-4xl text-textcolor my-6 font-black relative">{language.subtitles}</h2>
|
||||
|
||||
{#if mode === 'whisperLocal'}
|
||||
<span class="text-textcolor text-lg mt-4">{language.sourceLanguage}</span>
|
||||
<SelectInput value={sourceLang === null ? 'auto' : sourceLang}>
|
||||
<OptionInput value="auto">Auto</OptionInput>
|
||||
{#each getLanguageCodes() as lang}
|
||||
<OptionInput value={lang.code}>{lang.name}</OptionInput>
|
||||
{/each}
|
||||
</SelectInput>
|
||||
{/if}
|
||||
|
||||
|
||||
<span class="text-textcolor text-lg mt-4">{language.destinationLanguage}</span>
|
||||
<TextInput bind:value={selLang} />
|
||||
|
||||
<span class="text-textcolor text-lg mt-4">{language.prompt}</span>
|
||||
<TextAreaInput bind:value={prompt} />
|
||||
|
||||
<span class="text-textcolor text-lg mt-4">{language.type}</span>
|
||||
<SelectInput bind:value={mode} onchange={(e) => {
|
||||
if(mode === 'llm'){
|
||||
prompt = LLMModePrompt
|
||||
}
|
||||
if(mode === 'whisper' || mode === 'whisperLocal'){
|
||||
prompt = WhisperModePrompt
|
||||
}
|
||||
}}>
|
||||
<OptionInput value="llm">LLM</OptionInput>
|
||||
<OptionInput value="whisper">Whisper</OptionInput>
|
||||
<OptionInput value="whisperLocal">Whisper Local</OptionInput>
|
||||
</SelectInput>
|
||||
|
||||
{#if !(modelInfo.flags.includes(LLMFlags.hasAudioInput) && modelInfo.flags.includes(LLMFlags.hasVideoInput)) && mode === 'llm'}
|
||||
<span class="text-draculared text-lg mt-4">{language.subtitlesWarning1}</span>
|
||||
{/if}
|
||||
{#if !(modelInfo.flags.includes(LLMFlags.hasStreaming) && DBState.db.useStreaming)}
|
||||
<span class="text-draculared text-lg mt-4">{language.subtitlesWarning2}</span>
|
||||
{/if}
|
||||
{#if !('gpu' in navigator) && mode === 'whisperLocal'}
|
||||
<span class="text-draculared text-lg mt-4">{language.noWebGPU}</span>
|
||||
{/if}
|
||||
|
||||
{#if !outputText}
|
||||
<Button className="mt-4" onclick={() => {
|
||||
if(mode === 'llm'){
|
||||
runLLMMode()
|
||||
}
|
||||
if(mode === 'whisper' || mode === 'whisperLocal'){
|
||||
runWhisperMode()
|
||||
}
|
||||
}}>
|
||||
{language.run}
|
||||
</Button>
|
||||
{:else if vttB64 && fileB64}
|
||||
<details class="mt-4">
|
||||
<pre>{outputText}</pre>
|
||||
</details>
|
||||
{:else}
|
||||
<pre>{outputText}</pre>
|
||||
{/if}
|
||||
|
||||
{#if vttB64 && fileB64}
|
||||
<div class="mt-4">
|
||||
{#key vttB64}
|
||||
<video controls src={fileB64} class="w-full">
|
||||
<track default kind="captions" src={vttB64} srclang="en" />
|
||||
</video>
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
<span class="text-textcolor text-lg mt-4">{language.download}</span>
|
||||
|
||||
<Button className="mt-4" onclick={() => {
|
||||
outputText = ''
|
||||
fileB64 = ''
|
||||
vttB64 = ''
|
||||
}}>
|
||||
{language.reset}
|
||||
</Button>
|
||||
|
||||
<Button className="mt-4" onclick={async () => {
|
||||
const sel = parseInt(await alertSelect([
|
||||
'WebVTT',
|
||||
'SRT'
|
||||
]))
|
||||
const a = document.createElement('a')
|
||||
|
||||
// WebVTT
|
||||
if(sel === 0){
|
||||
downloadFile('subtitle.vtt', outputText)
|
||||
return
|
||||
}
|
||||
|
||||
// SRT
|
||||
if(sel === 1){
|
||||
downloadFile('subtitle.srt', webVttToSrt().join('\n\n'))
|
||||
return
|
||||
}
|
||||
}}>
|
||||
{language.download}
|
||||
</Button>
|
||||
{/if}
|
||||
@@ -51,6 +51,9 @@
|
||||
<span class="text-textcolor">{language.genTimes} <Help key="genTimes"/></span>
|
||||
<NumberInput marginBottom={true} size={"sm"} min={0} max={4096} bind:value={DBState.db.genTime}/>
|
||||
|
||||
<span class="text-textcolor">{language.assetMaxDifference}</span>
|
||||
<NumberInput marginBottom={true} size={"sm"} bind:value={DBState.db.assetMaxDifference}/>
|
||||
|
||||
<span class="text-textcolor mt-4">GPT Vision Quality <Help key="gptVisionQuality"/></span>
|
||||
<SelectInput bind:value={DBState.db.gptVisionQuality}>
|
||||
<OptionInput value="low">Low</OptionInput>
|
||||
|
||||
@@ -271,7 +271,7 @@
|
||||
|
||||
{#if submenu === 1 || submenu === -1}
|
||||
<span class="text-textcolor">{language.maxContextSize}</span>
|
||||
<NumberInput min={0} max={getModelMaxContext(DBState.db.aiModel)} marginBottom={true} bind:value={DBState.db.maxContext}/>
|
||||
<NumberInput min={0} marginBottom={true} bind:value={DBState.db.maxContext}/>
|
||||
|
||||
|
||||
<span class="text-textcolor">{language.maxResponseSize}</span>
|
||||
|
||||
@@ -363,6 +363,10 @@
|
||||
<Help key="betaMobileGUI"/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mt-2">
|
||||
<Check bind:check={DBState.db.menuSideBar} name={language.menuSideBar}/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mt-2">
|
||||
<Check bind:check={DBState.db.notification} name={language.notification} onChange={async (e) => {
|
||||
let hasPermission = {state: 'denied'}
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
{/if}
|
||||
|
||||
{#if DBState.db.translatorType === 'google'}
|
||||
<span class="text-textcolor mt-4">Translator Input Language</span>
|
||||
<span class="text-textcolor mt-4">{language.sourceLanguage}</span>
|
||||
<SelectInput className="mt-2 mb-4" bind:value={DBState.db.translatorInputLanguage}>
|
||||
<OptionInput value="auto">Auto</OptionInput>
|
||||
<OptionInput value="en">English</OptionInput>
|
||||
|
||||
@@ -121,6 +121,7 @@
|
||||
<OptionInput value="editprocess">{language.editProcess}</OptionInput>
|
||||
<OptionInput value="editdisplay">{language.editDisplay}</OptionInput>
|
||||
<OptionInput value="edittrans">{language.editTranslationDisplay}</OptionInput>
|
||||
<OptionInput value="disabled">{language.disabled}</OptionInput>
|
||||
</SelectInput>
|
||||
<span class="text-textcolor mt-6">IN:</span>
|
||||
<TextInput size="sm" bind:value={value.in} />
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
FolderOpenIcon,
|
||||
HomeIcon,
|
||||
WrenchIcon,
|
||||
User2Icon,
|
||||
} from "lucide-svelte";
|
||||
import {
|
||||
addCharacter,
|
||||
@@ -292,7 +293,79 @@
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
{#if DBState.db.menuSideBar}
|
||||
<div
|
||||
class="h-full w-20 min-w-20 flex-col items-center bg-bgcolor text-textcolor shadow-lg relative rs-sidebar"
|
||||
class:editMode
|
||||
class:risu-sub-sidebar={$sideBarClosing}
|
||||
class:risu-sub-sidebar-close={$sideBarClosing}
|
||||
class:hidden={hidden}
|
||||
class:flex={!hidden}
|
||||
>
|
||||
<button
|
||||
class="flex items-center justify-center py-2 flex-col gap-1 w-full mt-4"
|
||||
class:text-textcolor2={!(
|
||||
$selectedCharID < 0 &&
|
||||
$PlaygroundStore === 0 &&
|
||||
!$settingsOpen
|
||||
)}
|
||||
onclick={() => {
|
||||
reseter();
|
||||
selectedCharID.set(-1)
|
||||
PlaygroundStore.set(0)
|
||||
OpenRealmStore.set(false)
|
||||
}}
|
||||
>
|
||||
<HomeIcon />
|
||||
<span class="text-xs">{language.home}</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center justify-center py-2 flex-col gap-1 w-full"
|
||||
class:text-textcolor2={!$settingsOpen}
|
||||
onclick={() => {
|
||||
if ($settingsOpen) {
|
||||
reseter();
|
||||
settingsOpen.set(false);
|
||||
} else {
|
||||
reseter();
|
||||
settingsOpen.set(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Settings />
|
||||
<span class="text-xs">{language.settings}</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center justify-center py-2 flex-col gap-1 w-full"
|
||||
class:text-textcolor2={!(
|
||||
$selectedCharID >= 0
|
||||
)}
|
||||
onclick={() => {
|
||||
reseter();
|
||||
openGrid();
|
||||
|
||||
}}
|
||||
>
|
||||
<User2Icon />
|
||||
<span class="text-xs">{language.character}</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center justify-center py-2 flex-col gap-1 w-full"
|
||||
class:text-textcolor2={!(
|
||||
$selectedCharID < 0 &&
|
||||
$PlaygroundStore !== 0
|
||||
)}
|
||||
onclick={() => {
|
||||
reseter();
|
||||
selectedCharID.set(-1)
|
||||
PlaygroundStore.set(1)
|
||||
}}
|
||||
>
|
||||
<ShellIcon />
|
||||
<span class="text-xs">{language.playground}</span>
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="h-full w-20 min-w-20 flex-col items-center bg-bgcolor text-textcolor shadow-lg relative rs-sidebar"
|
||||
class:editMode
|
||||
@@ -561,6 +634,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="setting-area h-full flex-col overflow-y-auto overflow-x-hidden bg-darkbg py-6 text-textcolor max-h-full"
|
||||
class:risu-sidebar={!$sideBarClosing}
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
import CheckInput from "./GUI/CheckInput.svelte";
|
||||
import { ArrowDown, ArrowUp, XIcon } from "lucide-svelte";
|
||||
import TextInput from "./GUI/TextInput.svelte";
|
||||
|
||||
import { DBState } from 'src/ts/stores.svelte';
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
|
||||
let opened = $state(false)
|
||||
interface Props {
|
||||
promptItem: PromptItem;
|
||||
@@ -98,6 +99,20 @@
|
||||
|
||||
}
|
||||
|
||||
const EL = (e:KeyboardEvent) => {
|
||||
if(e.ctrlKey && e.altKey && e.key === 'o'){
|
||||
opened = !opened
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener('keydown', EL)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
document.removeEventListener('keydown', EL)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<div class="first:mt-0 w-full h-2" role="doc-pagebreak" ondrop={(e) => {
|
||||
|
||||
Reference in New Issue
Block a user