Skip to content

Commit

Permalink
fix: parser issue, and audio dispatching issue
Browse files Browse the repository at this point in the history
  • Loading branch information
nekomeowww committed Dec 9, 2024
1 parent 26c3794 commit 3735157
Show file tree
Hide file tree
Showing 9 changed files with 266 additions and 153 deletions.
1 change: 1 addition & 0 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ words:
- kwaa
- live2dcubismcore
- live2dcubismframework
- Llmmarker
- Myriam
- Neko
- nekomeowww
Expand Down
6 changes: 0 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,6 @@
"packages/*",
"docs"
],
"pnpm": {
"override": {
"@babel/preset-env": "7.26.0",
"workbox-build": "7.3.0"
}
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},
Expand Down
2 changes: 2 additions & 0 deletions packages/stage/src/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ declare global {
const useLLM: typeof import('./stores/llm')['useLLM']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLink: typeof import('vue-router/auto')['useLink']
const useLlmmarkerParser: typeof import('./composables/llmmarkerParser')['useLlmmarkerParser']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
Expand Down Expand Up @@ -503,6 +504,7 @@ declare module 'vue' {
readonly useLLM: UnwrapRef<typeof import('./stores/llm')['useLLM']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']>
readonly useLlmmarkerParser: UnwrapRef<typeof import('./composables/llmmarkerParser')['useLlmmarkerParser']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
Expand Down
58 changes: 19 additions & 39 deletions packages/stage/src/components/MainStage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { useLLM } from '../stores/llm'
import { useSettings } from '../stores/settings'
import BasicTextarea from './BasicTextarea.vue'
// import AudioWaveform from './AudioWaveform.vue'
import Live2DViewer from './Live2DViewer.vue'
import Settings from './Settings.vue'
import ThreeDScene from './ThreeDScene.vue'
Expand Down Expand Up @@ -107,7 +106,6 @@ const ttsQueue = useQueue<string>({
voice: 'Myriam',
// Beatrice is not 'childish' like the others
// voice: 'Beatrice',
// text: body.text,
model_id: 'eleven_multilingual_v2',
voice_settings: {
stability: 0.4,
Expand Down Expand Up @@ -150,13 +148,13 @@ const emotionsQueue = useQueue<Emotion>({
],
})
const emotionMessageContentQueue = useEmotionsMessageQueue(emotionsQueue, messageContentQueue)
const emotionMessageContentQueue = useEmotionsMessageQueue(emotionsQueue)
emotionMessageContentQueue.onHandlerEvent('emotion', (emotion) => {
// eslint-disable-next-line no-console
console.debug('emotion detected', emotion)
})
const delaysQueue = useDelayMessageQueue(emotionMessageContentQueue)
const delaysQueue = useDelayMessageQueue()
delaysQueue.onHandlerEvent('delay', (delay) => {
// eslint-disable-next-line no-console
console.debug('delay detected', delay)
Expand Down Expand Up @@ -216,49 +214,31 @@ async function onSendMessage(sendingMessage: string) {
live2DViewerRef.value?.setMotion(EmotionThinkMotionName)
const res = await stream(openAiApiBaseURL.value, openAiApiKey.value, openAIModel.value.id, messages.value.slice(0, messages.value.length - 1))
let fullText = ''
enum States {
Literal = 'literal',
Special = 'special',
}
let state = States.Literal
let buffer = ''
const parser = useLlmmarkerParser({
onLiteral: async (literal) => {
await messageContentQueue.add(literal)
streamingMessage.value.content += literal
},
onSpecial: async (special) => {
await delaysQueue.add(special)
await emotionMessageContentQueue.add(special)
},
})
for await (const textPart of asyncIteratorFromReadableStream(res.textStream, async v => v)) {
for (const textSingleChar of textPart) {
let newState: States = state
if (textSingleChar === '<')
newState = States.Special
else if (textSingleChar === '>')
newState = States.Literal
if (state === States.Literal && newState === States.Special) {
streamingMessage.value.content += buffer
buffer = ''
}
if (state === States.Special && newState === States.Literal)
buffer = '' // Clear buffer when exiting Special state
if (state === States.Literal && newState === States.Literal) {
streamingMessage.value.content += textSingleChar
buffer = ''
}
await delaysQueue.add(textSingleChar)
state = newState
buffer += textSingleChar
}
fullText += textPart
await parser.consume(textPart)
}
if (buffer)
streamingMessage.value.content += buffer
await parser.end()
await delaysQueue.add(llmInferenceEndToken)
messageInput.value = ''
// eslint-disable-next-line no-console
console.debug('Full text:', fullText)
}
watch([openAiApiBaseURL, openAiApiKey], async ([baseUrl, apiKey]) => {
Expand Down
144 changes: 144 additions & 0 deletions packages/stage/src/composables/llmmarkerParser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { describe, expect, it } from 'vitest'
import { useLlmmarkerParser } from './llmmarkerParser'

describe('useLlmmarkerParser', async () => {
it('should parse pure literals', async () => {
const fullText = 'Hello, world!'
const collectedLiterals: string[] = []
const collectedSpecials: string[] = []

const parser = useLlmmarkerParser({
onLiteral(literal) {
collectedLiterals.push(literal)
},
onSpecial(special) {
collectedSpecials.push(special)
},
})

for (const char of fullText) {
await parser.consume(char)
}

await parser.end()

expect(collectedLiterals).toEqual('Hello, world!'.split(''))
expect(collectedSpecials).toEqual([])
})

it('should parse pure specials', async () => {
const fullText = '<|Hello, world!|>'
const collectedLiterals: string[] = []
const collectedSpecials: string[] = []

const parser = useLlmmarkerParser({
onLiteral(literal) {
collectedLiterals.push(literal)
},
onSpecial(special) {
collectedSpecials.push(special)
},
})

for (const char of fullText) {
await parser.consume(char)
}

await parser.end()

expect(collectedLiterals).toEqual([])
expect(collectedSpecials).toEqual(['<|Hello, world!|>'])
})

it('should not include unfinished special', async () => {
const fullText = '<|Hello, world'
const collectedLiterals: string[] = []
const collectedSpecials: string[] = []

const parser = useLlmmarkerParser({
onLiteral(literal) {
collectedLiterals.push(literal)
},
onSpecial(special) {
collectedSpecials.push(special)
},
})

for (const char of fullText) {
await parser.consume(char)
}

await parser.end()

expect(collectedLiterals).toEqual([])
expect(collectedSpecials).toEqual([])
})

it('should parse with mixed input, ends with special', async () => {
const fullText = 'This is sentence 1, <|HELLO|> and this is sentence 2.<|WORLD|>'
const collectedLiterals: string[] = []
const collectedSpecials: string[] = []

const parser = useLlmmarkerParser({
onLiteral(literal) {
collectedLiterals.push(literal)
},
onSpecial(special) {
collectedSpecials.push(special)
},
})

for (const char of fullText) {
await parser.consume(char)
}

await parser.end()

expect(collectedLiterals).toEqual([...'This is sentence 1, '.split(''), ...' and this is sentence 2.'.split('')])
expect(collectedSpecials).toEqual(['<|HELLO|>', '<|WORLD|>'])
})

it('should parse correctly', async () => {
const testCases: { input: string, expectedLiterals: string[], expectedSpecials: string[] }[] = [
{
input: `<|A|> Wow, hello there!`,
expectedLiterals: ' Wow, hello there!'.split(''),
expectedSpecials: ['<|A|>'],
},
{
input: `<|A|> Hello!`,
expectedLiterals: ' Hello!'.split(''),
expectedSpecials: ['<|A|>'],
},
{
input: `<|A|> Hello! <|B|>`,
expectedLiterals: ' Hello! '.split(''),
expectedSpecials: ['<|A|>', '<|B|>'],
},
]

for (const tc of testCases) {
const { input, expectedLiterals, expectedSpecials } = tc
const collectedLiterals: string[] = []
const collectedSpecials: string[] = []

const parser = useLlmmarkerParser({
onLiteral(literal) {
collectedLiterals.push(literal)
},
onSpecial(special) {
collectedSpecials.push(special)
},
})

for (const char of input) {
await parser.consume(char)
}

await parser.end()

expect(collectedLiterals).toEqual(expectedLiterals)
expect(collectedSpecials).toEqual(expectedSpecials)
}
})
})
86 changes: 86 additions & 0 deletions packages/stage/src/composables/llmmarkerParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
enum States {
Literal = 'literal',
Special = 'special',
}

function peek(array: string, index: number, offset: number): string | undefined {
if (index + offset < 0 || index + offset >= (array.length - 1))
return ''

return array[index + offset]
}

export function useLlmmarkerParser(options: {
onLiteral?: (literal: string) => void | Promise<void>
onSpecial?: (special: string) => void | Promise<void>
}) {
let state = States.Literal
let buffer = ''

return {
async consume(textPart: string) {
for (let i = 0; i < textPart.length; i++) {
let current = textPart[i]
let newState: States = state

// read
if (current === '<' && peek(textPart, i, 1) === '|') {
current += peek(textPart, i, 1)
newState = States.Special
i++
}
else if (current === '|' && peek(textPart, i, 1) === '>') {
current += peek(textPart, i, 1)
newState = States.Literal
i++
}
else if (current === '<') {
newState = States.Special
}
else if (current === '>') {
newState = States.Literal
}

// handle
if (state === States.Literal && newState === States.Special) {
if (buffer !== '') {
await options.onLiteral?.(buffer)
buffer = ''
}
}
else if (state === States.Special && newState === States.Literal) {
if (buffer !== '') {
buffer += current
await options.onSpecial?.(buffer)
buffer = '' // Clear buffer when exiting Special state
}
}

if (state === States.Literal && newState === States.Literal) {
await options.onLiteral?.(current)
buffer = ''
}
else if (state === States.Special && newState === States.Literal) {
buffer = ''
}
else {
buffer += current
}

state = newState
}
},
async end() {
if (buffer !== '') {
if (state === States.Literal) {
await options.onLiteral?.(buffer)
}
else {
if (buffer.endsWith('|>')) {
await options.onSpecial?.(buffer)
}
}
}
},
}
}
Loading

0 comments on commit 3735157

Please sign in to comment.