mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-09 09:20:54 +00:00
Define love agent (#7098)
This commit is contained in:
parent
b063e98965
commit
bcc09dc00e
15
services/ai-bot/love-agent/.eslintrc.cjs
Normal file
15
services/ai-bot/love-agent/.eslintrc.cjs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
module.exports = {
|
||||||
|
"extends": [
|
||||||
|
"standard-with-typescript"
|
||||||
|
],
|
||||||
|
"ignorePatterns": ["*.json", "node_modules/*", ".eslintrc.cjs", "esbuild.config.js"],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/array-type": "off",
|
||||||
|
"@typescript-eslint/promise-function-async": "off",
|
||||||
|
"@typescript-eslint/consistent-type-imports": "off"
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: './tsconfig.json',
|
||||||
|
}
|
||||||
|
}
|
12
services/ai-bot/love-agent/.prettierrc
Normal file
12
services/ai-bot/love-agent/.prettierrc
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 120,
|
||||||
|
"useTabs": false,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"plugins": []
|
||||||
|
}
|
25
services/ai-bot/love-agent/Dockerfile
Normal file
25
services/ai-bot/love-agent/Dockerfile
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
FROM node:20
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
RUN npm install --ignore-scripts=false --verbose bufferutil utf-8-validate @mongodb-js/zstd snappy --unsafe-perm
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install libjemalloc2
|
||||||
|
RUN apt-get clean
|
||||||
|
|
||||||
|
ENV LD_PRELOAD=libjemalloc.so.2
|
||||||
|
ENV MALLOC_CONF=dirty_decay_ms:1000,narenas:2,background_thread:true
|
||||||
|
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
|
||||||
|
COPY package.json package.json
|
||||||
|
COPY pnpm-lock.yaml pnpm-lock.yaml
|
||||||
|
|
||||||
|
RUN pnpm install
|
||||||
|
COPY ./lib .
|
||||||
|
|
||||||
|
EXPOSE 4012
|
||||||
|
CMD [ "node", "index.js" , "start"]
|
15
services/ai-bot/love-agent/esbuild.config.js
Normal file
15
services/ai-bot/love-agent/esbuild.config.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import esbuild from 'esbuild';
|
||||||
|
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: ['src/index.ts', 'src/start.ts', 'src/config.ts', 'src/agent.ts', 'src/stt.ts'],
|
||||||
|
platform: 'node',
|
||||||
|
bundle: false,
|
||||||
|
minify: false,
|
||||||
|
outdir: 'lib',
|
||||||
|
keepNames: true,
|
||||||
|
sourcemap: 'inline',
|
||||||
|
allowOverwrite: true,
|
||||||
|
loader: {
|
||||||
|
'.ts': 'ts',
|
||||||
|
},
|
||||||
|
}).catch(() => process.exit(1))
|
40
services/ai-bot/love-agent/package.json
Normal file
40
services/ai-bot/love-agent/package.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "@hcengineering/love-agent",
|
||||||
|
"version": "0.6.0",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"types": "types/index.d.ts",
|
||||||
|
"author": "Anticrm Platform Contributors",
|
||||||
|
"license": "EPL-2.0",
|
||||||
|
"files": [
|
||||||
|
"lib/**/*",
|
||||||
|
"types/**/*",
|
||||||
|
"tsconfig.json"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "node esbuild.config.js",
|
||||||
|
"lint": "eslint src/**/*.ts",
|
||||||
|
"lint:fix": "eslint --fix src/**/*.ts",
|
||||||
|
"format": "pnpm lint:fix && prettier --write src/**/*.ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "~20.11.16",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||||
|
"@typescript-eslint/parser": "^6.11.0",
|
||||||
|
"esbuild": "^0.20.2",
|
||||||
|
"eslint": "^8.54.0",
|
||||||
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-n": "^15.4.0",
|
||||||
|
"eslint-plugin-promise": "^6.1.1",
|
||||||
|
"prettier": "^3.1.0",
|
||||||
|
"typescript": "^5.6.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@deepgram/sdk": "^3.8.1",
|
||||||
|
"@livekit/agents": "^0.3.3",
|
||||||
|
"@livekit/rtc-node": "^0.9.2",
|
||||||
|
"dotenv": "^16.4.5"
|
||||||
|
}
|
||||||
|
}
|
2892
services/ai-bot/love-agent/pnpm-lock.yaml
Normal file
2892
services/ai-bot/love-agent/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
105
services/ai-bot/love-agent/src/agent.ts
Normal file
105
services/ai-bot/love-agent/src/agent.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2024 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import { cli, defineAgent, type JobContext, WorkerOptions, WorkerPermissions } from '@livekit/agents'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { RemoteParticipant, RemoteTrack, RemoteTrackPublication, RoomEvent, TrackKind } from '@livekit/rtc-node'
|
||||||
|
|
||||||
|
import { STT } from './stt.js'
|
||||||
|
import { Metadata } from './type.js'
|
||||||
|
|
||||||
|
function parseMetadata (metadata: string): Metadata {
|
||||||
|
try {
|
||||||
|
return JSON.parse(metadata) as Metadata
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing metadata', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMetadata (data: string, stt: STT): void {
|
||||||
|
if (data === '') return
|
||||||
|
const metadata = parseMetadata(data)
|
||||||
|
|
||||||
|
if (metadata.language != null) {
|
||||||
|
stt.updateLanguage(metadata.language)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.transcription === true) {
|
||||||
|
stt.start()
|
||||||
|
} else if (metadata.transcription === false) {
|
||||||
|
stt.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineAgent({
|
||||||
|
entry: async (ctx: JobContext) => {
|
||||||
|
await ctx.connect()
|
||||||
|
await ctx.waitForParticipant()
|
||||||
|
|
||||||
|
const stt = new STT(ctx.room.name)
|
||||||
|
|
||||||
|
applyMetadata(ctx.room.metadata, stt)
|
||||||
|
|
||||||
|
ctx.room.on(RoomEvent.RoomMetadataChanged, (data) => {
|
||||||
|
applyMetadata(data, stt)
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.room.on(
|
||||||
|
RoomEvent.TrackSubscribed,
|
||||||
|
(track: RemoteTrack, publication: RemoteTrackPublication, participant: RemoteParticipant) => {
|
||||||
|
if (publication.kind === TrackKind.KIND_AUDIO) {
|
||||||
|
stt.subscribe(track, publication, participant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx.room.on(
|
||||||
|
RoomEvent.TrackUnsubscribed,
|
||||||
|
(track: RemoteTrack, publication: RemoteTrackPublication, participant: RemoteParticipant) => {
|
||||||
|
if (publication.kind === TrackKind.KIND_AUDIO) {
|
||||||
|
stt.unsubscribe(track, publication, participant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx.room.on(
|
||||||
|
RoomEvent.TrackMuted,
|
||||||
|
(publication) => {
|
||||||
|
if (publication.kind === TrackKind.KIND_AUDIO) {
|
||||||
|
stt.mute(publication.sid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx.room.on(
|
||||||
|
RoomEvent.TrackUnmuted,
|
||||||
|
(publication) => {
|
||||||
|
if (publication.kind === TrackKind.KIND_AUDIO) {
|
||||||
|
stt.unmute(publication.sid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx.addShutdownCallback(async () => {
|
||||||
|
stt.close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function runAgent (): void {
|
||||||
|
cli.runApp(new WorkerOptions({ agent: fileURLToPath(import.meta.url), permissions: new WorkerPermissions(true, true, true, true, [], true) }))
|
||||||
|
}
|
44
services/ai-bot/love-agent/src/config.ts
Normal file
44
services/ai-bot/love-agent/src/config.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2024 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
Port: number
|
||||||
|
TranscriptDelay: number
|
||||||
|
DeepgramApiKey: string
|
||||||
|
PlatformUrl: string
|
||||||
|
PlatformToken: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined)
|
||||||
|
|
||||||
|
const config: Config = (() => {
|
||||||
|
const params: Partial<Config> = {
|
||||||
|
Port: parseNumber(process.env.PORT) ?? 4020,
|
||||||
|
DeepgramApiKey: process.env.DEEPGRAM_API_KEY,
|
||||||
|
TranscriptDelay: parseNumber(process.env.TRANSCRIPT_DELAY) ?? 3000,
|
||||||
|
PlatformUrl: process.env.PLATFORM_URL,
|
||||||
|
PlatformToken: process.env.PLATFORM_TOKEN
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingEnv = (Object.keys(params) as Array<keyof Config>).filter((key) => params[key] === undefined)
|
||||||
|
|
||||||
|
if (missingEnv.length > 0) {
|
||||||
|
throw Error(`Missing env variables: ${missingEnv.join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params as Config
|
||||||
|
})()
|
||||||
|
|
||||||
|
export default config
|
21
services/ai-bot/love-agent/src/index.ts
Normal file
21
services/ai-bot/love-agent/src/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2024 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import { config } from 'dotenv'
|
||||||
|
|
||||||
|
import { start } from './start.js'
|
||||||
|
|
||||||
|
config()
|
||||||
|
void start()
|
33
services/ai-bot/love-agent/src/start.ts
Normal file
33
services/ai-bot/love-agent/src/start.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2024 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import config from './config.js'
|
||||||
|
import { runAgent } from './agent.js'
|
||||||
|
|
||||||
|
export const start = async (): Promise<void> => {
|
||||||
|
console.log('Starting love worker', config)
|
||||||
|
|
||||||
|
runAgent()
|
||||||
|
const onClose = (): void => {}
|
||||||
|
|
||||||
|
process.on('SIGINT', onClose)
|
||||||
|
process.on('SIGTERM', onClose)
|
||||||
|
process.on('uncaughtException', (e: Error) => {
|
||||||
|
console.error(e)
|
||||||
|
})
|
||||||
|
process.on('unhandledRejection', (e: Error) => {
|
||||||
|
console.error(e)
|
||||||
|
})
|
||||||
|
}
|
252
services/ai-bot/love-agent/src/stt.ts
Normal file
252
services/ai-bot/love-agent/src/stt.ts
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2024 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import { AudioStream, RemoteParticipant, RemoteTrack, RemoteTrackPublication } from '@livekit/rtc-node'
|
||||||
|
import {
|
||||||
|
createClient,
|
||||||
|
DeepgramClient,
|
||||||
|
ListenLiveClient,
|
||||||
|
LiveTranscriptionEvents,
|
||||||
|
LiveTranscriptionEvent,
|
||||||
|
LiveSchema
|
||||||
|
} from '@deepgram/sdk'
|
||||||
|
|
||||||
|
import config from './config.js'
|
||||||
|
|
||||||
|
const KEEP_ALIVE_INTERVAL = 10 * 1000
|
||||||
|
|
||||||
|
const dgSchema: LiveSchema = {
|
||||||
|
model: 'nova-2-general',
|
||||||
|
encoding: 'linear16',
|
||||||
|
smart_format: true,
|
||||||
|
endpointing: 500,
|
||||||
|
interim_results: true,
|
||||||
|
vad_events: true,
|
||||||
|
utterance_end_ms: 1000,
|
||||||
|
|
||||||
|
punctuate: true,
|
||||||
|
language: 'en'
|
||||||
|
}
|
||||||
|
|
||||||
|
export class STT {
|
||||||
|
private readonly deepgram: DeepgramClient
|
||||||
|
|
||||||
|
private isInProgress = false
|
||||||
|
private language: string = 'en'
|
||||||
|
|
||||||
|
private readonly trackBySid = new Map<string, RemoteTrack>()
|
||||||
|
private readonly streamBySid = new Map<string, AudioStream>()
|
||||||
|
private readonly mutedTracks = new Set<string>()
|
||||||
|
private readonly participantBySid = new Map<string, RemoteParticipant>()
|
||||||
|
|
||||||
|
private readonly dgConnectionBySid = new Map<string, ListenLiveClient>()
|
||||||
|
private readonly intervalBySid = new Map<string, NodeJS.Timeout>()
|
||||||
|
|
||||||
|
private readonly transcriptsBySid = new Map<string, { value: string, startedOn: number }>()
|
||||||
|
|
||||||
|
private readonly interval: NodeJS.Timeout
|
||||||
|
|
||||||
|
constructor (private readonly name: string) {
|
||||||
|
this.deepgram = createClient(config.DeepgramApiKey)
|
||||||
|
this.interval = this.interval = setInterval(() => {
|
||||||
|
this.sendTranscriptToPlatform()
|
||||||
|
}, config.TranscriptDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTranscriptToPlatform (): void {
|
||||||
|
const now = Date.now()
|
||||||
|
for (const [sid, transcript] of this.transcriptsBySid.entries()) {
|
||||||
|
if (now - transcript.startedOn > config.TranscriptDelay) {
|
||||||
|
void this.sendToPlatform(transcript.value, sid)
|
||||||
|
this.transcriptsBySid.delete(sid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLanguage (language: string): void {
|
||||||
|
const shouldRestart = (this.language ?? 'en') !== language
|
||||||
|
this.language = language
|
||||||
|
if (shouldRestart) {
|
||||||
|
this.stop()
|
||||||
|
this.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start (): void {
|
||||||
|
if (this.isInProgress) return
|
||||||
|
this.isInProgress = true
|
||||||
|
|
||||||
|
for (const sid of this.trackBySid.keys()) {
|
||||||
|
this.processTrack(sid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop (): void {
|
||||||
|
if (!this.isInProgress) return
|
||||||
|
this.isInProgress = false
|
||||||
|
for (const sid of this.trackBySid.keys()) {
|
||||||
|
this.stopDeepgram(sid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mute (sid: string): void {
|
||||||
|
this.mutedTracks.add(sid)
|
||||||
|
}
|
||||||
|
|
||||||
|
unmute (sid: string): void {
|
||||||
|
this.mutedTracks.delete(sid)
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe (track: RemoteTrack, publication: RemoteTrackPublication, participant: RemoteParticipant): void {
|
||||||
|
if (this.trackBySid.has(publication.sid)) return
|
||||||
|
this.trackBySid.set(publication.sid, track)
|
||||||
|
this.participantBySid.set(publication.sid, participant)
|
||||||
|
if (track.muted) {
|
||||||
|
this.mutedTracks.add(publication.sid)
|
||||||
|
}
|
||||||
|
if (this.isInProgress) {
|
||||||
|
this.processTrack(publication.sid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe (
|
||||||
|
_: RemoteTrack | undefined,
|
||||||
|
publication: RemoteTrackPublication,
|
||||||
|
participant: RemoteParticipant
|
||||||
|
): void {
|
||||||
|
this.trackBySid.delete(publication.sid)
|
||||||
|
this.participantBySid.delete(participant.sid)
|
||||||
|
this.mutedTracks.delete(publication.sid)
|
||||||
|
this.stopDeepgram(publication.sid)
|
||||||
|
}
|
||||||
|
|
||||||
|
stopDeepgram (sid: string): void {
|
||||||
|
const stream = this.streamBySid.get(sid)
|
||||||
|
if (stream !== undefined) {
|
||||||
|
stream.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const dgConnection = this.dgConnectionBySid.get(sid)
|
||||||
|
if (dgConnection !== undefined) {
|
||||||
|
dgConnection.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
const interval = this.intervalBySid.get(sid)
|
||||||
|
if (interval !== undefined) {
|
||||||
|
clearInterval(interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.intervalBySid.delete(sid)
|
||||||
|
this.dgConnectionBySid.delete(sid)
|
||||||
|
this.streamBySid.delete(sid)
|
||||||
|
}
|
||||||
|
|
||||||
|
processTrack (sid: string): void {
|
||||||
|
const track = this.trackBySid.get(sid)
|
||||||
|
if (track === undefined) return
|
||||||
|
if (this.dgConnectionBySid.has(sid)) return
|
||||||
|
|
||||||
|
const stream = new AudioStream(track)
|
||||||
|
const dgConnection = this.deepgram.listen.live({
|
||||||
|
...dgSchema,
|
||||||
|
channels: stream.numChannels,
|
||||||
|
sample_rate: stream.sampleRate,
|
||||||
|
language: this.language ?? 'en'
|
||||||
|
})
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
dgConnection.keepAlive()
|
||||||
|
}, KEEP_ALIVE_INTERVAL)
|
||||||
|
|
||||||
|
this.streamBySid.set(sid, stream)
|
||||||
|
this.dgConnectionBySid.set(track.sid, dgConnection)
|
||||||
|
this.intervalBySid.set(track.sid, interval)
|
||||||
|
|
||||||
|
dgConnection.on(LiveTranscriptionEvents.Open, () => {
|
||||||
|
dgConnection.on(LiveTranscriptionEvents.Transcript, (data: LiveTranscriptionEvent) => {
|
||||||
|
const transcript = data?.channel?.alternatives[0].transcript
|
||||||
|
if (transcript != null && transcript !== '') {
|
||||||
|
const prevData = this.transcriptsBySid.get(sid)
|
||||||
|
const prevValue = prevData?.value ?? ''
|
||||||
|
if (data.is_final === true) {
|
||||||
|
// TODO: how to join the final transcript ?
|
||||||
|
this.transcriptsBySid.set(sid, { value: prevValue + ' ' + transcript, startedOn: prevData?.startedOn ?? Date.now() })
|
||||||
|
}
|
||||||
|
if (data.speech_final === true) {
|
||||||
|
const result = this.transcriptsBySid.get(sid)?.value
|
||||||
|
if (result != null) {
|
||||||
|
void this.sendToPlatform(result, sid)
|
||||||
|
}
|
||||||
|
this.transcriptsBySid.delete(sid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
dgConnection.on(LiveTranscriptionEvents.Close, (d) => {
|
||||||
|
console.log('Connection closed.', d, track.sid)
|
||||||
|
})
|
||||||
|
|
||||||
|
dgConnection.on(LiveTranscriptionEvents.Error, (err) => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
void this.streamToDeepgram(sid, stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
async streamToDeepgram (sid: string, stream: AudioStream): Promise<void> {
|
||||||
|
for await (const frame of stream) {
|
||||||
|
if (!this.isInProgress) continue
|
||||||
|
if (this.mutedTracks.has(sid)) continue
|
||||||
|
const dgConnection = this.dgConnectionBySid.get(sid)
|
||||||
|
if (dgConnection === undefined) {
|
||||||
|
stream.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const buf = Buffer.from(frame.data.buffer)
|
||||||
|
dgConnection.send(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendToPlatform (transcript: string, sid: string): Promise<void> {
|
||||||
|
const request = {
|
||||||
|
transcript,
|
||||||
|
participant: this.participantBySid.get(sid)?.identity,
|
||||||
|
roomName: this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(`${config.PlatformUrl}/transcript`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer ' + config.PlatformToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request)
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error sending to platform', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close (): void {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
for (const sid of this.transcriptsBySid.keys()) {
|
||||||
|
this.trackBySid.delete(sid)
|
||||||
|
this.participantBySid.delete(sid)
|
||||||
|
this.stopDeepgram(sid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
services/ai-bot/love-agent/src/type.ts
Normal file
19
services/ai-bot/love-agent/src/type.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2024 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
export interface Metadata {
|
||||||
|
transcription?: boolean
|
||||||
|
language?: string
|
||||||
|
}
|
28
services/ai-bot/love-agent/tsconfig.json
Normal file
28
services/ai-bot/love-agent/tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"target": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"sourceMap": true,
|
||||||
|
|
||||||
|
"declaration": true,
|
||||||
|
"strict": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"skipDefaultLibCheck": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"disableReferencedProjectLoad": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"incremental": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./lib",
|
||||||
|
"declarationDir": "./types",
|
||||||
|
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user