This commit is contained in:
Dobromir Popov
2024-09-10 02:37:25 +03:00
25 changed files with 1986 additions and 82 deletions

15
.env
View File

@ -4,9 +4,11 @@ TTS_BACKEND_URL=https://api.tts.d-popov.com/
#TTS_BACKEND_URL=http://localhost:9001/asr #gpu 9002-cpu
TTS_BACKEND_URL2=http://localhost:9002/asr
TTS_BACKEND_URL3=http://192.168.0.10:9008/asr #gpu
TTS_BACKEND_URL4=http://192.168.0.10:9009/asr #cpu 9008-gpu
WS_URL=ws://localhost:8081
SERVER_PORT_WS=8081
#! TTS_BACKEND_URL4=http://192.168.0.10:9009/asr #cpu 9008-gpu
# WS_URL=ws://localhost:8080
PUBLIC_HOSTNAME=tts.d-popov.com
WS_URL=wss://tts.d-popov.com
SERVER_PORT_WS=8080
SERVER_PORT_HTTP=3005
# aider
@ -14,14 +16,15 @@ AIDER_MODEL=
AIDER_4=false
#AIDER_35TURBO=
# OPENAI_API_KEY=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
# OPENAI_API_KEY=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
# OPENAI_API_BASE=https://api.deepseek.com/v1
# OPENAI_API_KEY=sk-99df7736351f4536bd72cd64a416318a
# AIDER_MODEL=deepseek-coder #deepseek-coder, deepseek-chat
GROQ_API_KEY=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
aider --model groq/llama3-70b-8192
OPENAI_API_KEY=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
# aider --model groq/llama3-70b-8192
# List models available from Groq
aider --models groq/
# aider --models groq/

View File

@ -1,6 +1,23 @@
TTS_BACKEND_URL=http://192.168.0.10:9008/asr
WS_URL=ws://192.168.0.10:9008:8081
SERVER_PORT_WS=8081
SERVER_PORT_HTTP=8080
# TTS_BACKEND_URL=http://192.168.0.10:9008/asr
# WS_URL=ws://192.168.0.10:9008
# SERVER_PORT_WS=8081
# SERVER_PORT_HTTP=8080
ENV_NAME=demo
TTS_API_URL=https://api.tts.d-popov.com/asr
# LLN_MODEL=qwen2
# LNN_API_URL=https://ollama.d-popov.com/api/generate
LLN_MODEL=qwen2
LNN_API_URL=https://ollama.d-popov.com/api/generate
GROQ_API_KEY=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
OPENAI_API_KEY=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
WS_URL=wss://tts.d-popov.com
PUBLIC_HOSTNAME=tts.d-popov.com
SERVER_PORT_HTTP=8080
SERVER_PORT_WS=8081

16
.env.development Normal file
View File

@ -0,0 +1,16 @@
ENV_NAME=development
TTS_API_URL=https://api.tts.d-popov.com/asr
# LLN_MODEL=qwen2
# LNN_API_URL=https://ollama.d-popov.com/api/generate
LLN_MODEL=qwen2
LNN_API_URL=https://ollama.d-popov.com/api/generate
GROQ_API_KEY=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
OPENAI_API_KEY=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
WS_URL=ws://localhost:8080
SERVER_PORT_WS=8080
SERVER_PORT_HTTP=8080

View File

@ -2,7 +2,7 @@
TTS_BACKEND_URL=http://localhost:9001/asr #gpu 9002-cpu
TTS_BACKEND_URL2=http://localhost:9002/asr
TTS_BACKEND_URL3=http://192.168.0.10:9008/asr #gpu
TTS_BACKEND_URL4=http://192.168.0.10:9009/asr #cpu 9008-gpu
#! TTS_BACKEND_URL4=http://192.168.0.10:9009/asr #cpu 9008-gpu
WS_URL=ws://localhost:8081
SERVER_PORT_WS=8081
SERVER_PORT_HTTP=8080

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ agent-mobile/jdk/*
agent-mobile/artimobile/supervisord.pid
agent-pyter/lag-llama
agent-pyter/google-chrome-stable_current_amd64.deb
web/.node-persist/*

31
.vscode/launch.json vendored
View File

@ -27,10 +27,37 @@
}
},
{
"name": "node: Launch server.js",
"name": "start chat-server.js",
"type": "node",
"request": "launch",
"program": "conda activate node && node web/server.js",
// "program": "${workspaceFolder}/web/chat-server.js",
"runtimeExecutable": "npm", // Use npm to run the script
"runtimeArgs": [
"run",
"start:demo-chat" // The script to run
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"env": {
"NODE_ENV": "demo"
"OPENAI_API_KEY":
},
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "Launch server.js",
"type": "node",
"request": "launch",
// "program": "conda activate node && ${workspaceFolder}/web/server.js",
"program": "${workspaceFolder}/web/server.js",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"env": {
"CONDA_ENV": "node", //?
"NODE_ENV": "development"
},
"skipFiles": [
"<node_internals>/**"
]

28
.vscode/tasks.json vendored
View File

@ -86,17 +86,19 @@
"panel": "new"
},
},
{
"label": "conda-activate",
"type": "shell",
"command": "source ~/miniconda3/etc/profile.d/conda.sh && conda activate py && echo 'Activated Conda Environment (py)!'",
"problemMatcher": [],
"options": {
"shell": {
"executable": "/bin/bash",
"args": ["-c"]
}
}
}
]
{
"label": "conda-activate",
"type": "shell",
"command": "source ~/miniconda3/etc/profile.d/conda.sh && conda activate ${input:condaEnv} && echo 'Activated Conda Environment (${input:condaEnv})!'",
"problemMatcher": [],
}
],
"inputs": [
{
"id": "condaEnv",
"type": "promptString",
"description": "Enter the Conda environment name",
"default": "py"
}
]
}

View File

@ -60,7 +60,7 @@ ENV NODE_ENV=demo
# RUN apk update && apk add bash
RUN apk update && apk add git
RUN npm install -g npm@latest
#RUN npm install -g npm@latest
WORKDIR /app
@ -69,6 +69,7 @@ COPY ["package.json", "package-lock.json*", "npm-shrinkwrap.json*", "./"]
# && mv node_modules ../
COPY . .
RUN npm install
#RUN mpm install nodemon
EXPOSE 8080 8081

View File

@ -0,0 +1,11 @@
import ccxt
import pandas as pd
exchange = ccxt.coinbase()
symbol = 'BTC/USDT'
timeframe = '1m'
ohlcv = exchange.fetch_ohlcv(symbol, timeframe)
df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
df.set_index('timestamp', inplace=True)
# print(df.head())
print(df)

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
import os
import sys
from pprint import pprint
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.append(root + '/python')
import ccxt # noqa: E402
print('CCXT Version:', ccxt.__version__)
for exchange_id in ccxt.exchanges:
try:
exchange = getattr(ccxt, exchange_id)()
print(exchange_id)
# do what you want with this exchange
# pprint(dir(exchange))
except Exception as e:
print(e)

View File

@ -0,0 +1,99 @@
//@version=5
// https://www.youtube.com/watch?v=3fBLZgWSsy4
strategy("NNFX Style Strategy with ADX, EMA, ATR SL and TP", overlay=true)
// SSL Channel
period = input.int(title="SSL Period", defval=140)
smaHigh = ta.sma(high, period)
smaLow = ta.sma(low, period)
var float Hlv = na
Hlv := close > smaHigh ? 1 : close < smaLow ? -1 : nz(Hlv[1])
sslDown = Hlv < 0 ? smaHigh : smaLow
sslUp = Hlv < 0 ? smaLow : smaHigh
plot(sslDown, linewidth=2, color=color.red)
plot(sslUp, linewidth=2, color=color.lime)
// T3 Indicator
length_fast = input.int(40, minval=1, title="Fast T3 Length")
length_slow = input.int(90, minval=1, title="Slow T3 Length")
b = 0.7
t3(x, length) =>
e1 = ta.ema(x, length)
e2 = ta.ema(e1, length)
e3 = ta.ema(e2, length)
e4 = ta.ema(e3, length)
e5 = ta.ema(e4, length)
e6 = ta.ema(e5, length)
c1 = -b * b * b
c2 = 3 * b * b + 3 * b * b * b
c3 = -6 * b * b - 3 * b - 3 * b * b * b
c4 = 1 + 3 * b + b * b * b + 3 * b * b
c1 * e6 + c2 * e5 + c3 * e4 + c4 * e3
t3_fast = t3(close, length_fast)
t3_slow = t3(close, length_slow)
plot(t3_fast, color=color.blue, title="T3 Fast")
plot(t3_slow, color=color.red, title="T3 Slow")
// ADX Calculation
adxlen = input.int(100, title="ADX Smoothing")
dilen = input.int(110, title="DI Length")
dirmov(len) =>
up = ta.change(high)
down = -ta.change(low)
plusDM = na(up) ? na : (up > down and up > 0 ? up : 0)
minusDM = na(down) ? na : (down > up and down > 0 ? down : 0)
truerange = ta.rma(ta.tr(true), len)
plus = nz(100 * ta.rma(plusDM, len) / truerange)
minus = nz(100 * ta.rma(minusDM, len) / truerange)
[plus, minus]
adx(dilen, adxlen) =>
[plus, minus] = dirmov(dilen)
sum = plus + minus
adx = 100 * ta.rma(math.abs(plus - minus) / (sum == 0 ? 1 : sum), adxlen)
adx
adx_value = adx(dilen, adxlen)
adx_ema_length = input.int(80, title="ADX EMA Length")
adx_ema = ta.ema(adx_value, adx_ema_length)
plot(adx_value, title="ADX", color=color.orange)
plot(adx_ema, title="ADX EMA", color=color.purple)
// ATR-based Stop Loss and Take Profit
atr_length = input.int(120, title="ATR Length")
atr_stop_loss_multiplier = input.float(10, title="ATR Stop Loss Multiplier")
atr_take_profit_multiplier = input.float(20, title="ATR Take Profit Multiplier")
atr = ta.atr(atr_length)
// Strategy Logic
longCondition = ta.crossover(t3_fast, t3_slow) and adx_value > adx_ema and Hlv > 0
shortCondition = ta.crossunder(t3_fast, t3_slow) and adx_value > adx_ema and Hlv < 0
exitLongCondition = ta.crossunder(t3_fast, t3_slow) or Hlv < 0
exitShortCondition = ta.crossover(t3_fast, t3_slow) or Hlv > 0
// Debug plots
plotshape(series=longCondition, location=location.belowbar, color=color.green, style=shape.labelup, text="LONG")
plotshape(series=shortCondition, location=location.abovebar, color=color.red, style=shape.labeldown, text="SHORT")
if (longCondition)
stopLoss = close - atr_stop_loss_multiplier * atr
takeProfit = close + atr_take_profit_multiplier * atr
strategy.entry("Long", strategy.long)
strategy.exit("Long TP/SL", from_entry="Long", stop=stopLoss, limit=takeProfit)
if (shortCondition)
stopLoss = close + atr_stop_loss_multiplier * atr
takeProfit = close - atr_take_profit_multiplier * atr
strategy.entry("Short", strategy.short)
strategy.exit("Short TP/SL", from_entry="Short", stop=stopLoss, limit=takeProfit)
if (exitLongCondition)
strategy.close("Long")
if (exitShortCondition)
strategy.close("Short")

View File

@ -0,0 +1,89 @@
import numpy as np
import pandas as pd
class NNFXStrategy:
def __init__(self, ssl_period=140, t3_fast_length=40, t3_slow_length=90,
adx_len=100, di_len=110, adx_ema_length=80,
atr_length=120, atr_stop_loss_multiplier=10, atr_take_profit_multiplier=20):
self.ssl_period = ssl_period
self.t3_fast_length = t3_fast_length
self.t3_slow_length = t3_slow_length
self.adx_len = adx_len
self.di_len = di_len
self.adx_ema_length = adx_ema_length
self.atr_length = atr_length
self.atr_stop_loss_multiplier = atr_stop_loss_multiplier
self.atr_take_profit_multiplier = atr_take_profit_multiplier
def sma(self, series, period):
return series.rolling(window=period).mean()
def t3(self, series, length, b=0.7):
e1 = series.ewm(span=length).mean()
e2 = e1.ewm(span=length).mean()
e3 = e2.ewm(span=length).mean()
e4 = e3.ewm(span=length).mean()
e5 = e4.ewm(span=length).mean()
e6 = e5.ewm(span=length).mean()
c1 = -b * b * b
c2 = 3 * b * b + 3 * b * b * b
c3 = -6 * b * b - 3 * b - 3 * b * b * b
c4 = 1 + 3 * b + b * b * b + 3 * b * b
return c1 * e6 + c2 * e5 + c3 * e4 + c4 * e3
def adx(self, high, low, close, di_len, adx_len):
plus_dm = high.diff().clip(lower=0)
minus_dm = low.diff().clip(upper=0).abs()
tr = np.maximum.reduce([high - low, (high - close.shift()).abs(), (low - close.shift()).abs()])
atr = tr.rolling(window=di_len).mean()
plus_di = 100 * (plus_dm.rolling(window=di_len).mean() / atr)
minus_di = 100 * (minus_dm.rolling(window=di_len).mean() / atr)
dx = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di)
adx = dx.rolling(window=adx_len).mean()
adx_ema = adx.ewm(span=self.adx_ema_length).mean()
return adx, adx_ema
def atr(self, high, low, close, atr_length):
tr = np.maximum.reduce([high - low, (high - close.shift()).abs(), (low - close.shift()).abs()])
return tr.rolling(window=atr_length).mean()
def generate_signals(self, data):
data['sma_high'] = self.sma(data['high'], self.ssl_period)
data['sma_low'] = self.sma(data['low'], self.ssl_period)
data['hlv'] = np.where(data['close'] > data['sma_high'], 1, np.where(data['close'] < data['sma_low'], -1, np.nan))
data['hlv'] = data['hlv'].ffill().fillna(0)
data['ssl_down'] = np.where(data['hlv'] < 0, data['sma_high'], data['sma_low'])
data['ssl_up'] = np.where(data['hlv'] < 0, data['sma_low'], data['sma_high'])
data['t3_fast'] = self.t3(data['close'], self.t3_fast_length)
data['t3_slow'] = self.t3(data['close'], self.t3_slow_length)
data['adx'], data['adx_ema'] = self.adx(data['high'], data['low'], data['close'], self.di_len, self.adx_len)
data['atr'] = self.atr(data['high'], data['low'], data['close'], self.atr_length)
data['long_condition'] = (data['t3_fast'] > data['t3_slow']) & (data['adx'] > data['adx_ema']) & (data['hlv'] > 0)
data['short_condition'] = (data['t3_fast'] < data['t3_slow']) & (data['adx'] > data['adx_ema']) & (data['hlv'] < 0)
data['exit_long_condition'] = (data['t3_fast'] < data['t3_slow']) | (data['hlv'] < 0)
data['exit_short_condition'] = (data['t3_fast'] > data['t3_slow']) | (data['hlv'] > 0)
return data
def apply_strategy(self, data):
data = self.generate_signals(data)
trades = []
for i in range(1, len(data)):
if data['long_condition'].iloc[i]:
stop_loss = data['close'].iloc[i] - self.atr_stop_loss_multiplier * data['atr'].iloc[i]
take_profit = data['close'].iloc[i] + self.atr_take_profit_multiplier * data['atr'].iloc[i]
trades.append(('long', data.index[i], stop_loss, take_profit))
elif data['short_condition'].iloc[i]:
stop_loss = data['close'].iloc[i] + self.atr_stop_loss_multiplier * data['atr'].iloc[i]
take_profit = data['close'].iloc[i] - self.atr_take_profit_multiplier * data['atr'].iloc[i]
trades.append(('short', data.index[i], stop_loss, take_profit))
elif data['exit_long_condition'].iloc[i]:
trades.append(('exit_long', data.index[i]))
elif data['exit_short_condition'].iloc[i]:
trades.append(('exit_short', data.index[i]))
return trades

9
_doc/_notes/readme.md Normal file
View File

@ -0,0 +1,9 @@
# build:
docker build -t kevin-ai .
# start the project in container:
docker-compose up
# for debugging:
docker-compose -f docker-compose.debug.yml up
# demo (only node)
# docker-compose -f docker-compose.demo.yml up

View File

@ -8,7 +8,13 @@ services:
dockerfile: ./Dockerfile
environment:
NODE_ENV: development
# TTS_BACKEND_URL: https://tts.d-popov.com/asr
TTS_BACKEND_URL: http://192.168.0.10:9009/asr
WS_URL: wss://ws.ai.d-popov.com
SERVER_PORT_WS: 3001
SERVER_PORT_HTTP: 3000
ports:
- 3000:3000
- 23000:3000
- 23001:3001
- 9229:9229
command: ["node", "--inspect=0.0.0.0:9229", "web/server.js"]

15
docker-compose.demo.yml Normal file
View File

@ -0,0 +1,15 @@
version: '3.8'
services:
node-app:
container_name: node-voice-chat
build:
context: .
dockerfile: web/deploy/demo.Dockerfile
ports:
- "8880:8080" # Exposes port 3000 on the host and maps it to port 3000 on the container
volumes:
- .:/usr/src/app # Mounts the current directory to /usr/src/app in the container
environment:
NODE_ENV: demo # Sets the environment variable NODE_ENV to development
command: npm run start:demo-chat # Runs npm start when the container starts

View File

@ -1,18 +1,33 @@
version: '3.4'
services:
kevinai:
# kevinai:
# image: kevinai
# container_name: kevinai-dev
# build:
# context: .
# dockerfile: ./Dockerfile
# environment:
# NODE_ENV: production
# # TTS_BACKEND_URL: http://192.168.0.10:9009/asr
# WS_URL: ws://192.168.0.10:28081
# SERVER_PORT_WS: 8081
# SERVER_PORT_HTTP: 8080
# ports:
# - 28081:8081
# - 28080:8080
mlchat:
image: kevinai
container_name: kevinai-dev
container_name: kevinai-chat
build:
context: .
dockerfile: ./Dockerfile
environment:
NODE_ENV: production
TTS_BACKEND_URL: http://192.168.0.10:9009/asr
WS_URL: ws://192.168.0.10:28081
NODE_ENV: demo
# TTS_BACKEND_URL: http://192.168.0.10:9009/asr
WS_URL: wss://tts.d-popov.com
SERVER_PORT_WS: 8081
SERVER_PORT_HTTP: 8080
ports:
- 28081:8081
- 28080:8080
- 28081:8081

270
package-lock.json generated
View File

@ -8,15 +8,60 @@
"name": "kevin-ai",
"version": "1.0.0",
"dependencies": {
"axios": "^1.7.2",
"body-parser": "^1.20.2",
"dotenv": "^16.0.3",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"git": "^0.1.5",
"groq-sdk": "^0.4.0",
"node-persist": "^3.1.3",
"ollama": "^0.5.1",
"openai": "^4.50.0",
"request": "^2.88.2",
"ws": "^8.12.1"
}
},
"node_modules/@types/node": {
"version": "18.19.34",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz",
"integrity": "sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g==",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/node-fetch": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz",
"integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.0"
}
},
"node_modules/@types/node-fetch/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -29,6 +74,17 @@
"node": ">= 0.6"
}
},
"node_modules/agentkeepalive": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
"integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
"dependencies": {
"humanize-ms": "^1.2.1"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -83,6 +139,29 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz",
"integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg=="
},
"node_modules/axios": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@ -246,11 +325,14 @@
}
},
"node_modules/dotenv": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/ecc-jsbn": {
@ -288,6 +370,14 @@
"node": ">= 0.6"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
@ -406,6 +496,25 @@
"node": ">= 0.8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@ -427,6 +536,31 @@
"node": ">= 0.12"
}
},
"node_modules/form-data-encoder": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="
},
"node_modules/formdata-node": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
"dependencies": {
"node-domexception": "1.0.0",
"web-streams-polyfill": "4.0.0-beta.3"
},
"engines": {
"node": ">= 12.20"
}
},
"node_modules/formdata-node/node_modules/web-streams-polyfill": {
"version": "4.0.0-beta.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
"engines": {
"node": ">= 14"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -485,6 +619,21 @@
"resolved": "https://registry.npmjs.org/mime/-/mime-1.2.9.tgz",
"integrity": "sha512-WiLgbHTIq5AYUvU/Luli4mZ1bUcHpGNHyCsbl+KPMg4zt+XUDpQehWjuBjdLaEvDTinvKj/FgfQt3fPoT7j08g=="
},
"node_modules/groq-sdk": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/groq-sdk/-/groq-sdk-0.4.0.tgz",
"integrity": "sha512-h79q9sv4hcOBESR05N5eqHlGhAug9H9lr3EIiB+37ysWWekeG+KYQDK2lIIHYCm6O9LzgZzO/VdLdPP298+T0w==",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7",
"web-streams-polyfill": "^3.2.1"
}
},
"node_modules/har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
@ -557,6 +706,14 @@
"npm": ">=1.3.7"
}
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"dependencies": {
"ms": "^2.0.0"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -689,6 +846,43 @@
"node": ">= 0.6"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-gyp-build": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz",
@ -725,6 +919,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/ollama": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.1.tgz",
"integrity": "sha512-mAiCHxdvu63E8EFopz0y82QG7rGfYmKAWgmjG2C7soiRuz/Sj3r/ebvCOp+jasiCubqUPE0ZThKT5LR6wrrPtA==",
"dependencies": {
"whatwg-fetch": "^3.6.20"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@ -736,6 +938,24 @@
"node": ">= 0.8"
}
},
"node_modules/openai": {
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.50.0.tgz",
"integrity": "sha512-2ADkNIU6Q589oYHr5pn9k7SbUcrBTK9X0rIXrYqwMVSoqOj1yK9/1OO0ExaWsqOOpD7o58UmRjeKlx9gKAcuKQ==",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7",
"web-streams-polyfill": "^3.2.1"
},
"bin": {
"openai": "bin/cli"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -766,6 +986,11 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@ -990,6 +1215,11 @@
"node": ">=0.8"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@ -1018,6 +1248,11 @@
"node": ">= 0.6"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -1086,6 +1321,33 @@
"extsprintf": "^1.2.0"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"engines": {
"node": ">= 8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-fetch": {
"version": "3.6.20",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/ws": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz",

View File

@ -5,14 +5,23 @@
"scripts": {
"start": "node web/server.js",
"start:demo": "NODE_ENV=demo node web/server.js",
"start:demo-chat": "node web/chat-server.js",
"start:tele": "python agent-py-bot/agent.py"
},
},
"env": {
"NODE_ENV": "demo"
},
"dependencies": {
"axios": "^1.7.2",
"body-parser": "^1.20.2",
"dotenv": "^16.0.3",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"git": "^0.1.5",
"groq-sdk": "^0.4.0",
"node-persist": "^3.1.3",
"ollama": "^0.5.1",
"openai": "^4.50.0",
"request": "^2.88.2",
"ws": "^8.12.1"
}

23
web/.env Normal file
View File

@ -0,0 +1,23 @@
# TTS_BACKEND_URL=http://192.168.0.10:9008/asr
# WS_URL=ws://192.168.0.10:9008
# SERVER_PORT_WS=8081
# SERVER_PORT_HTTP=8080
ENV_NAME=development
TTS_API_URL=https://api.tts.d-popov.com/asr
# LLN_MODEL=qwen2
# LNN_API_URL=https://ollama.d-popov.com/api/generate
LLN_MODEL=qwen2
LNN_API_URL=https://ollama.d-popov.com/api/generate
GROQ_API_KEY=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
OPENAI_API_KEY=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
WS_URL=wss://tts.d-popov.com
PUBLIC_HOSTNAME=tts.d-popov.com
SERVER_PORT_WS=8080
SERVER_PORT_HTTP=8080

201
web/audio.js Normal file
View File

@ -0,0 +1,201 @@
let selectedDeviceId = "default";
export let serverTime;
export let recordButton;
export let socket;
let audioRecorder;
let audioStream;
let recording = false;
let connectionStatus;
let statusRecording;
let audioContext;
let volumeChecker;
let lastVolumes = new Array(5);
let averageVolume;
let silenceCount = 0;
let isSpeaking = false;
let soundDetected = false;
let speakingCount = 0;
let analyser = null;
let SILENCE_DELAY_MS = 50;
let preDetect_IncludedAudio = 400; //ms
let soundCount_Threshold = 10;
let silenceCount_Threshold = 10;
const volumeHistory = [];
export function setSocket(newSocket) {
socket = newSocket;
}
export function setRecordButton(newRecordButton) {
recordButton = newRecordButton;
recordButton.addEventListener("click", toggleListening);
}
export function InitAudioAnalyser(stream) {
audioContext = new AudioContext();
const source = audioContext.createMediaStreamSource(stream);
analyser = audioContext.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.8;
source.connect(analyser);
}
export function startListening() {
recording = true;
navigator.mediaDevices.getUserMedia({ audio: { sampleRate: 16000 } })
.then((stream) => {
audioStream = stream;
const audioContext = new AudioContext();
const sourceNode = audioContext.createMediaStreamSource(audioStream);
const audioSampleRate = sourceNode.context.sampleRate;
info.innerHTML = "Sample rate: " + audioSampleRate + " Hz";
var preBuffer = [];
const channelSplitter = audioContext.createChannelSplitter(2);
const channelMerger = audioContext.createChannelMerger(1);
sourceNode.connect(channelSplitter);
channelSplitter.connect(channelMerger, 0, 0);
const outputNode = channelMerger;
const mediaStreamDestination = audioContext.createMediaStreamDestination();
outputNode.connect(mediaStreamDestination);
const singleChannelStream = mediaStreamDestination.stream;
audioRecorder = new MediaRecorder(singleChannelStream);
audioRecorder.start();
audioRecorder.addEventListener("dataavailable", (event) => {
if (!soundDetected && autosend.checked) {
preBuffer = [];
preBuffer.push(event.data);
return;
}
if (event.data.size > 0) {
let data = event.data;
if (preBuffer.length > 0) {
sendAudioToServerPost(preBuffer);
}
sendAudioToServer(data);
soundDetected = false;
}
});
InitAudioAnalyser(stream);
});
recordButton.innerHTML = "Stop Talking";
recordButton.classList.toggle('bg-red-500');
recordButton.classList.toggle('bg-blue-500');
recordButton.classList.toggle('hover:bg-blue-700');
}
export function stopListening() {
recording = false;
audioRecorder.stop();
recordButton.innerHTML = "Push to Talk";
recordButton.classList.toggle('bg-blue-500');
recordButton.classList.toggle('bg-red-500');
recordButton.classList.toggle('hover:bg-blue-700');
clearInterval(volumeChecker);
if (audioStream) {
audioStream.getTracks().forEach(track => track.stop());
audioStream = null;
}
}
export function sendAudioToServerPost(data) {
const blob = new Blob(data, { type: "audio/ogg; codecs=opus" });
var formData = new FormData();
formData.append('file', data);
fetch('/upload', {
method: 'POST',
body: formData
});
}
export function sendAudioToServerJson(data) {
if (socket && socket.readyState === WebSocket.OPEN) {
const binaryData = Buffer.from(base64AudioData, 'base64');
socket.send(JSON.stringify({ type: 'audio', audiobase64: binaryData }));
serverTime = Date.now();
if (!autosend.checked) {
transcription.placeholder = "Processing audio...";
}
}
}
export function sendAudioToServer(data) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(data);
serverTime = Date.now();
if (!autosend.checked) {
transcription.placeholder = "Processing audio...";
}
}
}
export function toggleListening() {
if (socket.readyState === WebSocket.OPEN) {
if (recording) {
stopListening();
} else {
startListening();
}
}
}
export function initializeVolumeChecker() {
volumeChecker = setInterval(() => {
if (!audioContext) {
console.log("No audio context");
return;
}
const frequencyData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(frequencyData);
let totalVolume = 0;
for (let i = 0; i < frequencyData.length; i++) {
totalVolume += frequencyData[i];
}
averageVolume = totalVolume / frequencyData.length;
volumeHistory.push(averageVolume);
if (volumeHistory.length > 100) {
volumeHistory.shift();
}
const threshold = volumeHistory.reduce((acc, curr) => acc + curr) / volumeHistory.length + 5;
const isSilent = averageVolume < threshold;
if (averageVolume > threshold) {
if (autosend.checked && speakingCount == 0 && audioRecorder) {
soundDetected = false;
audioRecorder.stop();
audioRecorder.start();
}
speakingCount++;
if (speakingCount > soundCount_Threshold) {
statusRecording.innerHTML = "Listening...";
statusRecording.style.color = "green";
isSpeaking = true;
}
} else if (averageVolume - 5 < threshold) {
speakingCount = 0;
if (isSpeaking) {
silenceCount++;
if (silenceCount > silenceCount_Threshold) {
if (autosend.checked) {
soundDetected = true;
audioRecorder.stop();
audioRecorder.start();
}
isSpeaking = false;
statusRecording.innerHTML = "Silence detected...";
statusRecording.style.color = "orange";
}
}
}
}, SILENCE_DELAY_MS);
}

615
web/chat-client.html Normal file
View File

@ -0,0 +1,615 @@
<!DOCTYPE html>
<html>
<head>
<title>Real-time Voice Chat</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css">
</head>
<body class="bg-gray-100">
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-4 text-center">Real-time Voice Chat</h1>
<div class="flex justify-center items-center mb-4">
<!-- Username Input -->
<input type="text" id="username" class="border rounded p-2 mr-4" placeholder="Enter your username">
<div id="join-container" class="hidden">
<button id="btn-join" onclick="logInAndStoreSession()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Join Chat</button>
<select id="language-select">
<option value="auto">Auto</option>
<option value="en">English</option>
<option value="bg">Български</option>
<option value="fr">Français</option>
</select>
</div>
<!-- Clear Session Option -->
<button id="btn-disconnect" onclick="clearSession()"
class="hidden bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">Clear Session</button>
</div>
<!-- Active Users List -->
<div id="active-users-container" class="hidden flex justify-center items-center mb-4">
<div class="w-1/3">
<h2 class="text-xl font-bold mb-2">Active Users</h2>
<select id="users-list" class="w-full bg-white p-4 rounded shadow" multiple size="10">
<!-- Dynamic list of users -->
</select>
<button onclick="startChat()"
class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded mt-4">Start
Chat</button>
</div>
</div>
<div id="previous-chats-container" class="hidden w-2/3 mx-auto">
<h2 class="text-xl font-bold mb-2">Previous Chats</h2>
<div id="previous-chats" class="bg-white p-4 rounded shadow">
<!-- Previous chats content -->
</div>
</div>
<!-- Chat Room -->
<div id="chat-room-container" class="hidden w-2/3 mx-auto">
<h2 class="text-xl font-bold mb-2">Chat Room</h2>
<div id="chat-room" class="bg-white p-4 rounded shadow mb-4">
<!-- Chat room content -->
<div>
<div id="chat-room-users" class="flex flex-wrap mb-4">
<!-- Participants list -->
</div>
</div>
<div class="mb-4">
<label class="flex items-center space-x-2">
<input type="checkbox" id="autosend" class="mr-2">
<span>Continuous</span>
</label>
</div>
<div class="mb-4">
<button id="record-button" disabled
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-4">Push to
Talk</button>
</div>
<div id="status-recording" class="flex justify-center items-center mb-4">
</div>
<div id="transcription" class="border rounded p-4 h-48 overflow-y-scroll mb-4">
<!-- Transcription content -->
</div>
<canvas id="canvas" class="w-full mb-4"></canvas>
<div class="flex justify-between items-center">
<button id="copyButton"
class="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-2 px-4 rounded focus:outline-none"
onclick="copyToClipboard('transcription')">Copy</button>
<button id="clearButton"
class="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-2 px-4 rounded focus:outline-none"
onclick="clearTranscription()">Clear</button>
</div>
</div>
</div>
<!-- Connection Status and Info -->
<div class="flex justify-center items-center mb-4">
<div id="connection-status" class="mr-4"></div>
</div>
<div class="flex justify-center items-center mb-4">
<div id="info"></div>
</div>
<div id="status-recording" class="flex justify-center items-center mb-4"> status</div>
</div>
<script type="module">
// import * as audio from './audio.js';
let socket;
let sessionId;
let username;
let users = [];
let selectedUsers = [];
let chats = [];
let recordButton;
let connectionStatus;
let statusRecording;
let connected = false;
document.getElementById('autosend').addEventListener('change', (event) => {
const autosend = event.target.checked;
fetch('/settings', {
method: 'POST',
body: JSON.stringify({ autosend, sessionId }),
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin'
});
});
function connect() {
return new Promise((resolve, reject) => {
connectionStatus.innerHTML = "Connecting to WS...";
let wsurl = "ws://localhost:8080";
fetch("/wsurl")
.then((response) => response.text())
.then((data) => {
wsurl = data;
console.log("Got ws url: '" + wsurl + "'");
})
.then(() => {
socket = new WebSocket(wsurl);
// audio.setSocket(socket); // Set the socket in the audio module
socket.onopen = () => {
connectionStatus.innerHTML = "Connected to " + wsurl;
recordButton.disabled = false;
connected = true;
//if we stored a session id in a cookie, reconnect
const sessionId = getCookie("sessionId");
if (sessionId) {
socket.send(JSON.stringify({ type: 'reconnect', sessionId }));
}
else {
socket.send(JSON.stringify({ type: 'sessionId' }));
}
resolve(socket);
};
socket.onmessage = onmessage;
socket.onclose = () => {
connectionStatus.innerHTML = "Disconnected";
recordButton.disabled = true;
connected = false;
setTimeout(() => {
connect().then(resolve).catch(reject);
}, 5000);
};
})
.catch((error) => {
connectionStatus.innerHTML = "Error getting ws url: " + error;
reject(error);
});
});
};
function onmessage(event) {
try {
let json = JSON.parse(event.data);
switch (json.type) {
case "sessionId":
sessionId = json.sessionId;
//set the session id in the cookie
document.cookie = `sessionId=${sessionId}; path=/;`;
console.log("Got session id: " + sessionId);
break;
case "languageDetected":
statusRecording.innerHTML = "Detected language: " + json.languageDetected;
break;
case "text":
case "transcriptionResult":
transcription.innerHTML += "<br />" + json.text;
let latency = Date.now() - serverTime;
if (autosend.checked) {
// const arr = event.data.split(/[(\)]/);
// let queue = arr[1];
// let text = arr[2].trim();
// info.innerHTML = "latency: " + latency + "ms; server queue: " + queue + " requests";
//transcription.value += event.data + " ";
statusRecording.innerHTML = "Listening...";
statusRecording.style.color = "black";
} else {
//transcription.innerHTML = event.data;
}
break;
case 'audio':
const audioBuffer = Uint8Array.from(atob(json.audio), char => char.charCodeAt(0));
const audioBlob = new Blob([audioBuffer], { type: 'audio/mp3' });
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
audio.play();
break;
case "userList":
users = json.users;
updateUserList();
break;
case "chats":
chats = json.chats;
updateChatList();
break;
case "chat":
displayChatParticipants(json.chat.id, json.chat.participants);
break;
default:
console.log("Unknown message type:", json.type);
}
} catch (e) {
console.error("Failed to parse message", e);
}
}
function logInAndStoreSession() {
username = document.getElementById('username').value;
if (username.trim() === "") {
alert("Please enter a username");
return;
}
if (!socket || socket.readyState !== WebSocket.OPEN) {
connect().then(() => {
userJoin(sessionId, username, document.getElementById('language-select').value);
});
} else {
userJoin(sessionId, username, document.getElementById('language-select').value);
}
}
function userJoin(sessionId, username, language) {
socket.send(JSON.stringify({ type: 'join', username, language }));
document.cookie = `sessionId=${sessionId}; path=/;`;
document.cookie = `username=${username}; path=/;`;
showClearSessionOption();
}
function clearSession() {
document.cookie = "sessionId=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
location.reload();
}
function showClearSessionOption() {
const sessionId = getCookie("sessionId");
if (sessionId) {
if (!socket || socket.readyState !== WebSocket.OPEN) {
connect().then((s) => {
s.send(JSON.stringify({ type: 'reconnect', sessionId }));
});
}
document.getElementById('btn-disconnect').classList.remove('hidden');
document.getElementById('join-container').classList.add('hidden');
document.getElementById('active-users-container').classList.remove('hidden');
document.getElementById('previous-chats-container').classList.remove('hidden');
} else {
document.getElementById('btn-disconnect').classList.add('hidden');
document.getElementById('join-container').classList.remove('hidden');
}
}
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
window.onload = () => {
recordButton = document.getElementById("record-button");
setRecordButton(recordButton);
connectionStatus = document.getElementById("connection-status");
statusRecording = document.getElementById("status-recording");
showClearSessionOption();
connect().then(() => {
// audio.initializeVolumeChecker();
});
};
function copyToClipboard(id) {
var textarea = document.getElementById(id);
textarea.select();
document.execCommand('copy');
}
function clearTranscription() {
document.getElementById('transcription').innerText = '';
}
function updateUserList() {
const usersList = document.getElementById('users-list');
usersList.innerHTML = '';
users.forEach(user => {
const option = document.createElement('option');
option.value = user.sessionId;
option.innerText = "[" + user.language + "] " + user.username;
if (user.username === username) {
option.innerText += " (me)";
}
usersList.appendChild(option);
});
}
function startChat() {
const selectedOptions = Array.from(document.querySelectorAll('#users-list option:checked'));
selectedUsers = selectedOptions.map(option => option.value);
if (selectedUsers.length === 0) {
alert("Please select at least one user to start a chat.");
return;
}
selectedUsers.push(sessionId); // Add self to the selected users list for self-chat
socket.send(JSON.stringify({ type: 'startChat', users: selectedUsers }));
document.getElementById('chat-room-container').classList.remove('hidden');
}
function fetchPreviousChats(username) {
fetch(`/chats?username=${username}`)
.then(response => response.json())
.then(data => {
chats = data.chats;
updateChatList();
});
}
function updateChatList() {
const previousChats = document.getElementById('previous-chats');
previousChats.innerHTML = '';
chats.forEach(chat => {
const chatDiv = document.createElement('div');
chatDiv.classList.add('border', 'rounded', 'p-2', 'mb-2', 'cursor-pointer');
chatDiv.setAttribute('data-chat-id', chat.id); // Store chat ID in data attribute
const participants = chat.participants.join(', ');
const status = chat.participants.map(participant => {
const user = users.find(u => u.username === participant);
return user ? `${participant} (online)` : `${participant} (offline)`;
}).join(', ');
chatDiv.innerHTML = `${status}`;
chatDiv.addEventListener('click', () => {
// Remove highlight from all chat divs
document.querySelectorAll('#previous-chats > div').forEach(div => {
div.classList.remove('bg-blue-100');
});
// Highlight selected chat div
chatDiv.classList.add('bg-blue-100');
selectChatRoom(chat.id);
});
previousChats.appendChild(chatDiv);
});
}
function selectChatRoom(chatId) {
const chat = chats.find(c => c.id === chatId);
if (!chat) return;
const chatRoomUsers = document.getElementById('chat-room-users');
chatRoomUsers.innerHTML = ''; // Clear existing content
socket.send(JSON.stringify({ type: 'enterChat', chatId }));
document.getElementById('chat-room-container').classList.remove('hidden');
// displayChatParticipants(chatId, chat.participants);
}
function displayChatParticipants(chatId, participants) {
const chatRoomUsers = document.getElementById('chat-room-users');
let participantsHtml = '<div class="flex flex-wrap mb-4">';
participants.forEach(participantId => {
const user = users.find(u => u.sessionId === participantId);
const status = user ? "online" : "offline";
const username = user ? user.username : "Unknown User";
participantsHtml += `<span class="inline-flex items-center px-3 py-0.5 rounded-full text-sm font-medium bg-${status === 'online' ? 'green' : 'gray'}-100 text-${status === 'online' ? 'green' : 'gray'}-800 mr-2 mb-2">
${username} (${status})
</span>`;
});
participantsHtml += '</div>';
chatRoomUsers.innerHTML = participantsHtml;
}
// REGION AUDIO RECORDING
let selectedDeviceId = "default";
export let serverTime;
// export let recordButton;
// export let socket;
// let connectionStatus;
// let statusRecording;
let audioRecorder;
let audioStream;
let recording = false;
let audioContext;
let volumeChecker;
let lastVolumes = new Array(5);
let averageVolume;
let silenceCount = 0;
let isSpeaking = false;
let soundDetected = false;
let speakingCount = 0;
let analyser = null;
let SILENCE_DELAY_MS = 50;
let preDetect_IncludedAudio = 400; //ms
let soundCount_Threshold = 10;
let silenceCount_Threshold = 10;
const volumeHistory = [];
function setSocket(newSocket) {
socket = newSocket;
}
export function setRecordButton(newRecordButton) {
recordButton = newRecordButton;
recordButton.addEventListener("click", toggleListening);
}
export function InitAudioAnalyser(stream) {
audioContext = new AudioContext();
const source = audioContext.createMediaStreamSource(stream);
analyser = audioContext.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.8;
source.connect(analyser);
}
export function startListening() {
recording = true;
navigator.mediaDevices.getUserMedia({ audio: { sampleRate: 16000 } })
.then((stream) => {
audioStream = stream;
const audioContext = new AudioContext();
const sourceNode = audioContext.createMediaStreamSource(audioStream);
const audioSampleRate = sourceNode.context.sampleRate;
info.innerHTML = "Sample rate: " + audioSampleRate + " Hz";
var preBuffer = [];
const channelSplitter = audioContext.createChannelSplitter(2);
const channelMerger = audioContext.createChannelMerger(1);
sourceNode.connect(channelSplitter);
channelSplitter.connect(channelMerger, 0, 0);
const outputNode = channelMerger;
const mediaStreamDestination = audioContext.createMediaStreamDestination();
outputNode.connect(mediaStreamDestination);
const singleChannelStream = mediaStreamDestination.stream;
audioRecorder = new MediaRecorder(singleChannelStream);
audioRecorder.start();
audioRecorder.addEventListener("dataavailable", (event) => {
if (!soundDetected && autosend.checked) {
preBuffer = [];
preBuffer.push(event.data);
return;
}
if (event.data.size > 0) {
let data = event.data;
if (preBuffer.length > 0) {
sendAudioToServerPost(preBuffer);
}
sendAudioToServer(data);
soundDetected = false;
}
});
InitAudioAnalyser(stream);
});
recordButton.innerHTML = "Stop Talking";
recordButton.classList.toggle('bg-red-500');
recordButton.classList.toggle('bg-blue-500');
recordButton.classList.toggle('hover:bg-blue-700');
}
export function stopListening() {
recording = false;
audioRecorder.stop();
recordButton.innerHTML = "Push to Talk";
recordButton.classList.toggle('bg-blue-500');
recordButton.classList.toggle('bg-red-500');
recordButton.classList.toggle('hover:bg-blue-700');
clearInterval(volumeChecker);
if (audioStream) {
audioStream.getTracks().forEach(track => track.stop());
audioStream = null;
}
}
export function sendAudioToServerPost(data) {
const blob = new Blob(data, { type: "audio/ogg; codecs=opus" });
var formData = new FormData();
formData.append('file', data);
fetch('/upload', {
method: 'POST',
body: formData
});
}
export function sendAudioToServerJson(data) {
if (socket && socket.readyState === WebSocket.OPEN) {
const binaryData = Buffer.from(base64AudioData, 'base64');
socket.send(JSON.stringify({ type: 'audio', audiobase64: binaryData }));
serverTime = Date.now();
if (!autosend.checked) {
transcription.placeholder = "Processing audio...";
}
}
}
export function sendAudioToServer(data) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(data);
serverTime = Date.now();
if (!autosend.checked) {
transcription.placeholder = "Processing audio...";
}
}
}
export function toggleListening() {
if (socket.readyState === WebSocket.OPEN) {
if (recording) {
stopListening();
} else {
startListening();
}
}
}
export function initializeVolumeChecker() {
volumeChecker = setInterval(() => {
if (!audioContext) {
console.log("No audio context");
return;
}
const frequencyData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(frequencyData);
let totalVolume = 0;
for (let i = 0; i < frequencyData.length; i++) {
totalVolume += frequencyData[i];
}
averageVolume = totalVolume / frequencyData.length;
volumeHistory.push(averageVolume);
if (volumeHistory.length > 100) {
volumeHistory.shift();
}
const threshold = volumeHistory.reduce((acc, curr) => acc + curr) / volumeHistory.length + 5;
const isSilent = averageVolume < threshold;
if (averageVolume > threshold) {
if (autosend.checked && speakingCount == 0 && audioRecorder) {
soundDetected = false;
audioRecorder.stop();
audioRecorder.start();
}
speakingCount++;
if (speakingCount > soundCount_Threshold) {
statusRecording.innerHTML = "Listening...";
statusRecording.style.color = "green";
isSpeaking = true;
}
} else if (averageVolume - 5 < threshold) {
speakingCount = 0;
if (isSpeaking) {
silenceCount++;
if (silenceCount > silenceCount_Threshold) {
if (autosend.checked) {
soundDetected = true;
audioRecorder.stop();
audioRecorder.start();
}
isSpeaking = false;
statusRecording.innerHTML = "Silence detected...";
statusRecording.style.color = "orange";
}
}
}
}, SILENCE_DELAY_MS);
}
// Expose functions to global scope
window.logInAndStoreSession = logInAndStoreSession;
window.clearSession = clearSession;
window.copyToClipboard = copyToClipboard;
window.clearTranscription = clearTranscription;
window.startChat = startChat;
</script>
<script src="https://cdn.webrtc-experiment.com/MediaStreamRecorder.js"></script>
</body>
</html>

403
web/chat-server.js Normal file
View File

@ -0,0 +1,403 @@
const express = require('express');
const bodyParser = require('body-parser');
const WebSocket = require('ws');
const storage = require('node-persist');
const request = require('request');
const fs = require('fs');
const path = require('path');
const dotenv = require('dotenv');
const ollama = require('ollama');
const axios = require('axios');
const OpenAI = require('openai');
const Groq = require('groq-sdk');
// Load environment variables
dotenv.config({ path: `.env${process.env.NODE_ENV === 'development' ? '.development' :'.'+ process.env.NODE_ENV }` });
// Initialize services
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const groq = new Groq({ apiKey: process.env.GROQ_API_KEY });
// Express setup
const app = express();
app.use(bodyParser.json());
// Configuration constants
const PORT_HTTP = process.env.SERVER_PORT_HTTP || 3000;
const PORT_WS = process.env.SERVER_PORT_WS || 8080;
const TTS_API_URL = process.env.TTS_API_URL;
const LNN_API_URL = process.env.LNN_API_URL;
const LLN_MODEL = process.env.LLN_MODEL;
let language = "en";
let storeRecordings = false;
let queueCounter = 0;
const sessions = new Map();
const chats = new Map(); // Store chat rooms
// Initialize storage and load initial values
async function initStorage() {
await storage.init();
language = await storage.getItem('language') || language;
storeRecordings = await storage.getItem('storeRecordings') || storeRecordings;
const storedChats = await storage.getItem('chats') || [];
storedChats.forEach(chat => chats.set(chat.id, chat));
const storedSessions = await storage.getItem('sessions') || [];
storedSessions.forEach(session => sessions.set(session.sessionId, session));
}
initStorage();
// WebSocket Server
const wss = new WebSocket.Server({ port: PORT_WS });
wss.on('connection', ws => {
ws.on('message', async message => handleMessage(ws, message));
ws.on('close', () => handleClose(ws));
});
// Handle WebSocket messages
async function handleMessage(ws, message) {
let data;
try {
data = JSON.parse(message);
} catch {
return handleAudioData(ws, message);
}
try {
switch (data.type) {
case 'sessionId':
await handleSessionId(ws);
break;
case 'join':
await handleJoin(ws, data);
break;
case 'startChat':
await handleStartChat(ws, data);
break;
case 'enterChat':
await handleEnterChat(ws, data);
break;
case 'reconnect':
await handleReconnect(ws, data);
break;
default:
console.log('Unknown message type:', data.type);
}
} catch (err) {
console.error('Failed to handle message', err);
}
}
function handleClose(ws) {
sessions.delete(ws.sessionId);
broadcastUserList();
}
// Handlers for specific message types
async function handleSessionId(ws) {
ws.sessionId = generateSessionId();
sessions.set(ws.sessionId, { language: 'en' });
await storage.setItem('sessions', Array.from(sessions.values()));
}
async function handleJoin(ws, { username, language }) {
sessions.set(ws.sessionId, { username, sessionId: ws.sessionId, language });
ws.send(JSON.stringify({ type: 'sessionId', sessionId: ws.sessionId, language, storeRecordings }));
const userChats = Array.from(chats.values()).filter(chat => chat.participants.includes(ws.sessionId));
ws.send(JSON.stringify({ type: 'chats', chats: userChats }));
broadcastUserList();
}
async function handleStartChat(ws, { users }) {
const chatId = generateChatId();
let participants = [ws.sessionId, ...users];
participants = [...new Set(participants)];
chats.set(chatId, { participants, messages: [] });
await storage.setItem('chats', Array.from(chats.values()));
notifyParticipants(participants);
broadcastUserList();
}
async function handleEnterChat(ws, { chatId }) {
const enteredChat = chats.get(chatId);
const currentSession = sessions.get(ws.sessionId);
currentSession.currentChat = chatId;
if (enteredChat && enteredChat.participants.includes(ws.sessionId)) {
ws.send(JSON.stringify({ type: 'chat', chat: enteredChat }));
}
}
async function handleReconnect(ws, { sessionId }) {
const userSession = sessions.get(sessionId);
if (userSession) {
sessions.set(ws.sessionId, userSession);
ws.sessionId = sessionId;
const userChats = Array.from(chats.values()).filter(chat => chat.participants.includes(ws.sessionId));
ws.send(JSON.stringify({ type: 'chats', chats: userChats }));
} else {
console.log('Session not found:', sessionId);
}
broadcastUserList();
}
// Utility functions
function generateSessionId() {
return Math.random().toString(36).substring(2);
}
function generateChatId() {
return Math.random().toString(36).substring(2);
}
function broadcastUserList() {
const userList = Array.from(sessions.values()).map(user => ({
username: user.username,
sessionId: user.sessionId,
currentChat: user.currentChat,
language: user.language
}));
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({ type: 'userList', users: userList }));
}
});
}
function notifyParticipants(participants) {
participants.forEach(sessionId => {
const participantSocket = Array.from(wss.clients).find(client => client.sessionId === sessionId);
if (participantSocket && participantSocket.readyState === WebSocket.OPEN) {
const userChats = Array.from(chats.entries())
.filter(([id, chat]) => chat.participants.includes(sessionId))
.map(([id, chat]) => ({ id, participants: chat.participants }));
participantSocket.send(JSON.stringify({ type: 'chats', chats: userChats }));
}
});
}
async function handleAudioData(ws, data) {
const sessionData = sessions.get(ws.sessionId);
let { language, task } = sessionData;
const formData = {
task: task || 'transcribe',
language,
vad_filter: 'true',
output: 'json',
audio_file: {
value: data,
options: { filename: 'audio.ogg', contentType: 'audio/ogg' }
}
};
if (!language || language === 'auto') {
await detectLanguage(ws, formData);
} else {
await transcribeAudio(ws, formData, sessionData);
}
}
async function detectLanguage(ws, formData) {
try {
const result = await requestPromise({
method: 'POST',
url: TTS_API_URL.replace('/asr', '/detect-language'),
formData
});
const { language_code } = JSON.parse(result);
if (language_code) {
const sessionData = sessions.get(ws.sessionId);
sessionData.language = language_code;
ws.send(JSON.stringify({ type: 'languageDetected', languageDetected: language_code }));
await transcribeAudio(ws, formData, sessionData);
}
} catch (err) {
console.error('Language detection failed:', err);
}
}
async function transcribeAudio(ws, formData, sessionData) {
const start = new Date().getTime();
queueCounter++;
try {
if(sessionData.language) {
formData.language = sessionData.language;
}
formData.vad_filter = 'true';
const body = await requestPromise({ method: 'POST', url: TTS_API_URL, formData });
queueCounter--;
const duration = new Date().getTime() - start;
ws.send(JSON.stringify({
type: 'text',
queueCounter,
duration,
language: sessionData.language,
text: body
}));
await handleChatTranscription(ws, body, sessionData);
} catch (err) {
console.error('Transcription failed:', err);
}
if (storeRecordings) {
const timestamp = Date.now();
fs.mkdir('rec', { recursive: true }, err => {
if (err) console.error(err);
else {
fs.writeFile(`rec/audio${timestamp}.ogg`, formData.audio_file.value, err => {
if (err) console.error(err);
else console.log(`Audio data saved to rec/audio${timestamp}.ogg`);
});
}
});
}
}
async function handleChatTranscription(ws, body, sessionData) {
if (sessionData.currentChat) {
const chat = chats.get(sessionData.currentChat);
if (chat) {
let msg = { sender: sessionData.username, text: body, translations: [] };
chat.messages.push(msg);
for (let sessionId of chat.participants) {
if (sessionId !== ws.sessionId) {
const targetLang = sessions.get(sessionId)?.language || 'en';
if (targetLang !== sessionData.language) {
const translation = await translateText(body, sessionData.language, targetLang);
msg.translations.push({ language: targetLang, text: translation });
const participantSocket = Array.from(wss.clients).find(client => client.sessionId === sessionId);
if (participantSocket && participantSocket.readyState === WebSocket.OPEN) {
participantSocket.send(JSON.stringify({ type: 'text', text: `${sessionData.username}: ${translation}` }));
const audioBuffer = await generateSpeech(translation);
participantSocket.send(JSON.stringify({ type: 'audio', audio: audioBuffer.toString('base64') }));
}
} else {
const participantSocket = Array.from(wss.clients).find(client => client.sessionId === sessionId);
if (participantSocket && participantSocket.readyState === WebSocket.OPEN) {
participantSocket.send(JSON.stringify({ type: 'text', text: `${sessionData.username}: ${body}` }));
participantSocket.send(JSON.stringify({ type: 'audio', audio: formData.toString('base64') }));
}
}
}
}
}
}
}
async function translateText(originalText, originalLanguage, targetLanguage) {
const prompt = `Translate this text from ${originalLanguage} to ${targetLanguage}: ${originalText}`;
const response = await groq.chat.completions.create({
messages: [
{
role: "system",
content: `You are translating voice transcriptions from '${originalLanguage}' to '${targetLanguage}'. Reply with just the translation.`,
},
{
role: "user",
content: originalText,
},
],
model: "llama3-8b-8192",
});
return response.choices[0]?.message?.content || "";
}
async function generateSpeech(text) {
const mp3 = await openai.audio.speech.create({
model: "tts-1",
voice: "alloy",
input: text,
});
return Buffer.from(await mp3.arrayBuffer());
}
// HTTP Server
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'chat-client.html'));
});
app.get('/audio.js', (req, res) => {
res.sendFile(path.join(__dirname, 'audio.js'));
});
app.post('/log', (req, res) => {
console.log(`[LOG ${new Date().toISOString()}] ${req.body.message}`);
res.status(200).send('OK');
});
app.get('/wsurl', (req, res) => {
if(process.env.PUBLIC_HOSTNAME){
process.env.WS_URL = `wss://${process.env.PUBLIC_HOSTNAME}`
}
console.log('Request for WS URL resolved with:', process.env.WS_URL );
res.status(200).send(process.env.WS_URL);
});
app.get('/settings', async (req, res) => {
if (req.query.language) {
language = req.query.language;
await storage.setItem('language', language);
}
if (req.query.storeRecordings) {
storeRecordings = req.query.storeRecordings;
await storage.setItem('storeRecordings', storeRecordings);
}
res.status(200).send({ language, storeRecordings });
});
app.post('/settings', async (req, res) => {
const { sessionId, language, storeRecordings, task } = req.body;
const sessionData = sessions.get(sessionId);
if (language) sessionData.language = language;
if (storeRecordings !== undefined) sessionData.storeRecordings = storeRecordings;
if (task) sessionData.task = task;
res.status(200).send('OK');
});
app.post('/upload', (req, res) => {
const timestamp = Date.now();
console.log('Received audio data:', timestamp);
fs.mkdir('rec', { recursive: true }, err => {
if (err) return res.status(500).send('ERROR');
const file = fs.createWriteStream(`rec/audio_slice_${timestamp}.ogg`);
req.pipe(file);
file.on('finish', () => res.status(200).send('OK'));
});
});
app.get('/chats', (req, res) => {
const { username } = req.query;
const userChats = Array.from(chats.values()).filter(chat => chat.participants.includes(username));
res.status(200).send({ chats: userChats });
});
app.listen(PORT_HTTP, () => {
console.log(`Server listening on port ${PORT_HTTP}`);
});
// Helper to wrap request in a promise
function requestPromise(options) {
return new Promise((resolve, reject) => {
request(options, (error, response, body) => {
if (error) return reject(error);
resolve(body);
});
});
}

View File

@ -3,11 +3,9 @@
<head>
<title>Real-time Speech-to-Text</title>
<meta name="viewport"
content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Add the Tailwind CSS library -->
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css">
</head>
<body class="bg-gray-100">
@ -15,65 +13,61 @@
<h1 class="text-2xl font-bold mb-4 text-center">Rt STT</h1>
<div class="flex justify-center items-center mb-4">
<label class="toggle flex items-center">
<input type="checkbox"
id="autosend"
class="mr-2">
<input type="checkbox" id="autosend" class="mr-2">
<span class="slider"></span>
<span class="ml-2">Continuous</span>
</label>
<select id="input-devices"
class="ml-4">
<select id="input-devices" class="ml-4">
<option value="default">Default</option>
</select>
<select id="language-select">
<option value="auto">Auto</option>
<option value="en">English</option>
<option value="bg">Български</option>
<option value="fr">Français</option>
</select>
<select id="task-select">
<option value="transcribe">Transcribe</option>
<option value="translate">Translate</option>
</select>
<label class="toggle flex items-center ml-4">
<input type="checkbox"
id="store-recordings"
class="mr-2">
<input type="checkbox" id="store-recordings" class="mr-2">
<span class="slider"></span>
<span class="ml-2">Store Recordings</span>
</div>
<div class="flex justify-center items-center mb-4">
<span id="record-actions">
<button id="record-button"
disabled
<button id="record-button" disabled
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-4">
Start Recording</button>
<button id="record-button-speakers"
disabled
<button id="record-button-speakers" disabled
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-4">
Stream from speakers</button>
</span>
</div>
<div class="flex justify-center items-center mb-4">
<div id="connection-status"
style="margin-right: 5px;"></div>
<div id="connection-status" style="margin-right: 5px;"></div>
</div>
<div class="flex justify-center items-center mb-4">
<div id="info"></div>
</div>
<div id="status-recording"
class="flex justify-center items-center mb-4">
<div id="status-recording" class="flex justify-center items-center mb-4">
</div>
<div class="relative rounded-lg border border-gray-300 shadow-sm">
<textarea id="transcription"
class="block w-full h-48 p-4 resize-none"
<textarea id="transcription" class="block w-full h-48 p-4 resize-none"
placeholder="Whisper something into the microphone..."></textarea>
<button id="copyButton"
class="absolute top-0 right-0 px-4 py-2 text-sm font-medium text-gray-700 bg-gray-200 hover:bg-gray-300 rounded-bl-lg focus:outline-none"
onclick="copyToClipboard('transcription')">
Copy
</button>
<button id="clearButton"
class="absolute top-0 right-20 px-2 py-1 text-sm font-medium text-gray-700 bg-gray-200 hover:bg-gray-300 rounded-br-lg focus:outline-none"
onclick="transcription.value = ''">
Clear
</button>
</div>
<canvas id="canvas"
class="w-full"></canvas>
<canvas id="canvas" class="w-full"></canvas>
<script>
let sessionId;
@ -274,15 +268,28 @@ disabled
if (json.hasOwnProperty("language")) {
languageSelect.value = json.language;
}
if (json.hasOwnProperty("languageDetected")) {
statusRecording.innerHTML = "Detected language: " + json.languageDetected;
}
if (json.hasOwnProperty("taskSelect")) {
taskSelect.value = json.taskSelect;
}
//storerecordings checkbox
if (json.hasOwnProperty("storeRecordings")) {
storeRecordings.checked = json.storeRecordings;
}
if (json.hasOwnProperty("text")) {
transcription.value += "\r\n" + json.text;
}
if (json.hasOwnProperty("queueCounter")) {
let latency = Date.now() - serverTime;
console.log("Received message from server: " + event.data + " (latency: " + latency + "ms)");
info.innerHTML = "latency: " + latency + "ms; server queue: " + queue + " requests";
}
return;
} catch (e) {
//not json
@ -425,7 +432,7 @@ disabled
serverTime = Date.now();
console.log("Sent some audio data to server.");
if (!autosend.checked) {
transcription.innerHTML = "Processing audio...";
transcription.placeholder = "Processing audio...";
}
} else {
console.log("Not connected, not sending audio data to server.");

View File

@ -0,0 +1,20 @@
# Use the official Node.js 14 image as a base image
FROM node:20
# Create and change to the app directory
WORKDIR /usr/src/app
# Copy the package.json and package-lock.json files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Expose port 3000
EXPOSE 8880
# Start the application
CMD ["npm", "run", "start:demo-chat"]

View File

@ -1,6 +1,7 @@
//load .env file
if (require('dotenv')) {
require('dotenv').config()
const envFile =process.env.NODE_ENV === 'development' ? '.env.development' : '.env';
require('dotenv').config({ path: envFile });
}
console.log('Starting ws server on port ' + process.env.SERVER_PORT_WS);
@ -9,8 +10,9 @@ const wss = new WebSocket.Server({ port: process.env.SERVER_PORT_WS });
// console.log("ENV="+process.env)
console.log("TTS_BACKEND_URL="+process.env.TTS_BACKEND_URL)
console.log("WS_URL="+process.env.WS_URL)
console.log("TTS_API_URL=" + process.env.TTS_API_URL)
console.log("WS_URL=" + process.env.WS_URL)
console.log("ENV_NAME=" + process.env.ENV_NAME)
let language = "en";
let storeRecordings = false;
@ -19,7 +21,7 @@ let queueCounter = 0;
const storage = require('node-persist');
storage.init().then(() => {
storage.getItem('language').then((value) => {
if (value != undefined) { language = value; console.log('language: ' + language); }
if (value != undefined) { language = value; console.log('stored language: ' + language); }
else { storage.setItem('language', language).then(() => { console.log('language set to ' + language + "(default)"); }); }
});
@ -51,11 +53,14 @@ wss.on('connection', (ws, req) => {
let language = sessionData?.language || 'en';
let task = sessionData?.task || 'transcribe';
//show the size of the audio data as 0.000 MB
console.log('(queue ' + queueCounter + ') Received ' + (data.length / 1024 / 1024).toFixed(3) + ' MB audio from client. Crrent language: ' + language);
console.log('(queue ' + queueCounter + ') Received ' + (data.length / 1024 / 1024).toFixed(3) + ' MB audio from client. Crrent language: ' + language, 'task: ' + task);
var request = require('request');
var endpoint = process.env.TTS_API_URL;
var formData = {
task: task,
language: sessionData.language,
language: language,
vad_filter: 'true',
output: 'json',
audio_file: {
value: data,
@ -65,6 +70,31 @@ wss.on('connection', (ws, req) => {
}
}
};
console.log('language:', language);
if (language == 'auto' || language == '') {
console.log('Detecting language...');
request.post({ url: endpoint.replace('/asr', '/detect-language'), formData: formData }, function optionalCallback(err, httpResponse, body) {
console.log('detected:', body);
if (typeof body === 'string') {
body = JSON.parse(body);
}
if (body && body.language_code) {
language = body.language_code; if (body && body.language_code) {
let language = body.language_code;
sessionData.language = language;
console.log('language set to:', language);
webSocket.send(JSON.stringify({ languageDetected: body.detected_language }));
} else {
console.error('Error: Invalid body or missing language_code');
}
sessionData.language = language;
console.log('language set to:', language);
} else {
console.error('Error: Invalid body or missing language_code');
}
});
}
storeRecordings = sessionData?.storeRecordings || storeRecordings;
if (storeRecordings) {
@ -85,7 +115,7 @@ wss.on('connection', (ws, req) => {
//record start time
var start = new Date().getTime();
queueCounter++;
request.post({ url: process.env.TTS_BACKEND_URL, formData: formData }, function optionalCallback(err, httpResponse, body) {
request.post({ url: process.env.TTS_API_URL, formData: formData }, function optionalCallback(err, httpResponse, body) {
queueCounter--;
if (err) {
return console.error('upload failed:', err);
@ -94,7 +124,8 @@ wss.on('connection', (ws, req) => {
var duration = new Date().getTime() - start;
//console.log('decoded (' + duration + 'ms):', body);
console.log('decoded (' + (duration / 1000).toFixed(2) + 's):', body);
webSocket.send("(" + queueCounter + ") " + body);
//webSocket.send("(" + queueCounter + ") " + body);
webSocket.send(JSON.stringify({ queueCounter: queueCounter, duration: duration, language: language, text: body}));
});
});
});
@ -158,7 +189,7 @@ app.post('/settings', (req, res) => {
sessionData.storeRecordings = body.storeRecordings;
console.log(`Session ${sid}: storeRecordings set to ${sessionData.storeRecordings}`);
}
if(body.task != undefined){
if (body.task != undefined) {
sessionData.task = body.task;
}
@ -210,14 +241,14 @@ app.get('/test_ocr', (req, res) => {
formData.language = req.query.language;
}
var tts_url = process.env.TTS_BACKEND_URL;
var tts_url = process.env.TTS_API_URL;
if (req.query.ttsID != undefined) {
//1: TTS_BACKEND_URL
//2: TTS_BACKEND_URL2
//3: TTS_BACKEND_URL3
//4: TTS_BACKEND_URL4
//1: TTS_API_URL
//2: TTS_API_URL2
//3: TTS_API_URL3
//4: TTS_API_URL4
if (req.query.ttsID !== '1') {
tts_url = process.env['TTS_BACKEND_URL' + req.query.ttsID];
tts_url = process.env['TTS_API_URL' + req.query.ttsID];
}
}