speechflow 1.7.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +425 -146
- package/etc/claude.md +5 -5
- package/etc/speechflow.yaml +2 -2
- package/package.json +3 -3
- package/speechflow-cli/dst/speechflow-main-api.js +6 -5
- package/speechflow-cli/dst/speechflow-main-api.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main-graph.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-main-graph.js +35 -13
- package/speechflow-cli/dst/speechflow-main-graph.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main-status.js +3 -7
- package/speechflow-cli/dst/speechflow-main-status.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +3 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +4 -2
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js +4 -2
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js +2 -2
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.js +1 -2
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js +32 -5
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.d.ts +0 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +1 -6
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.d.ts +0 -1
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +9 -9
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-google.d.ts +17 -0
- package/speechflow-cli/dst/speechflow-node-a2t-google.js +320 -0
- package/speechflow-cli/dst/speechflow-node-a2t-google.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js +6 -4
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +6 -11
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +6 -5
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-google.d.ts +15 -0
- package/speechflow-cli/dst/speechflow-node-t2a-google.js +218 -0
- package/speechflow-cli/dst/speechflow-node-t2a-google.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.d.ts +2 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +19 -6
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-openai.d.ts +15 -0
- package/speechflow-cli/dst/speechflow-node-t2a-openai.js +195 -0
- package/speechflow-cli/dst/speechflow-node-t2a-openai.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.d.ts +17 -0
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js +608 -0
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-amazon.js.map +1 -1
- package/speechflow-cli/dst/{speechflow-node-t2t-transformers.d.ts → speechflow-node-t2t-opus.d.ts} +1 -3
- package/speechflow-cli/dst/speechflow-node-t2t-opus.js +159 -0
- package/speechflow-cli/dst/speechflow-node-t2t-opus.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-profanity.d.ts +11 -0
- package/speechflow-cli/dst/speechflow-node-t2t-profanity.js +118 -0
- package/speechflow-cli/dst/speechflow-node-t2t-profanity.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-punctuation.d.ts +13 -0
- package/speechflow-cli/dst/speechflow-node-t2t-punctuation.js +220 -0
- package/speechflow-cli/dst/speechflow-node-t2t-punctuation.js.map +1 -0
- package/speechflow-cli/dst/{speechflow-node-t2t-openai.d.ts → speechflow-node-t2t-spellcheck.d.ts} +2 -2
- package/speechflow-cli/dst/{speechflow-node-t2t-openai.js → speechflow-node-t2t-spellcheck.js} +47 -99
- package/speechflow-cli/dst/speechflow-node-t2t-spellcheck.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +3 -6
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-summary.d.ts +16 -0
- package/speechflow-cli/dst/speechflow-node-t2t-summary.js +241 -0
- package/speechflow-cli/dst/speechflow-node-t2t-summary.js.map +1 -0
- package/speechflow-cli/dst/{speechflow-node-t2t-ollama.d.ts → speechflow-node-t2t-translate.d.ts} +2 -2
- package/speechflow-cli/dst/{speechflow-node-t2t-transformers.js → speechflow-node-t2t-translate.js} +53 -115
- package/speechflow-cli/dst/speechflow-node-t2t-translate.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-x2x-filter.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js +10 -0
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-device.js +3 -3
- package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-exec.d.ts +12 -0
- package/speechflow-cli/dst/speechflow-node-xio-exec.js +223 -0
- package/speechflow-cli/dst/speechflow-node-xio-exec.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-file.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-file.js +80 -67
- package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +2 -1
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-vban.d.ts +17 -0
- package/speechflow-cli/dst/speechflow-node-xio-vban.js +330 -0
- package/speechflow-cli/dst/speechflow-node-xio-vban.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.d.ts +39 -0
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.js +500 -0
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js +2 -1
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-audio.js +5 -6
- package/speechflow-cli/dst/speechflow-util-audio.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-error.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-error.js +5 -7
- package/speechflow-cli/dst/speechflow-util-error.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-llm.d.ts +35 -0
- package/speechflow-cli/dst/speechflow-util-llm.js +363 -0
- package/speechflow-cli/dst/speechflow-util-llm.js.map +1 -0
- package/speechflow-cli/dst/speechflow-util-misc.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-misc.js +4 -4
- package/speechflow-cli/dst/speechflow-util-misc.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-queue.js +3 -3
- package/speechflow-cli/dst/speechflow-util-queue.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-stream.js +4 -2
- package/speechflow-cli/dst/speechflow-util-stream.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-util.js +1 -0
- package/speechflow-cli/dst/speechflow-util.js.map +1 -1
- package/speechflow-cli/etc/oxlint.jsonc +2 -1
- package/speechflow-cli/package.json +34 -17
- package/speechflow-cli/src/lib.d.ts +5 -0
- package/speechflow-cli/src/speechflow-main-api.ts +6 -5
- package/speechflow-cli/src/speechflow-main-graph.ts +40 -13
- package/speechflow-cli/src/speechflow-main-status.ts +4 -8
- package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +4 -0
- package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +4 -2
- package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-expander.ts +4 -2
- package/speechflow-cli/src/speechflow-node-a2a-gender.ts +2 -2
- package/speechflow-cli/src/speechflow-node-a2a-pitch.ts +1 -2
- package/speechflow-cli/src/speechflow-node-a2a-wav.ts +33 -6
- package/speechflow-cli/src/speechflow-node-a2t-amazon.ts +6 -11
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +13 -12
- package/speechflow-cli/src/speechflow-node-a2t-google.ts +322 -0
- package/speechflow-cli/src/speechflow-node-a2t-openai.ts +8 -4
- package/speechflow-cli/src/speechflow-node-t2a-amazon.ts +7 -11
- package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +6 -5
- package/speechflow-cli/src/speechflow-node-t2a-google.ts +206 -0
- package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +22 -6
- package/speechflow-cli/src/speechflow-node-t2a-openai.ts +179 -0
- package/speechflow-cli/src/speechflow-node-t2a-supertonic.ts +701 -0
- package/speechflow-cli/src/speechflow-node-t2t-amazon.ts +2 -1
- package/speechflow-cli/src/speechflow-node-t2t-opus.ts +136 -0
- package/speechflow-cli/src/speechflow-node-t2t-profanity.ts +93 -0
- package/speechflow-cli/src/speechflow-node-t2t-punctuation.ts +201 -0
- package/speechflow-cli/src/{speechflow-node-t2t-openai.ts → speechflow-node-t2t-spellcheck.ts} +48 -107
- package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +3 -6
- package/speechflow-cli/src/speechflow-node-t2t-summary.ts +229 -0
- package/speechflow-cli/src/speechflow-node-t2t-translate.ts +181 -0
- package/speechflow-cli/src/speechflow-node-x2x-filter.ts +16 -3
- package/speechflow-cli/src/speechflow-node-x2x-trace.ts +3 -3
- package/speechflow-cli/src/speechflow-node-xio-device.ts +4 -7
- package/speechflow-cli/src/speechflow-node-xio-exec.ts +210 -0
- package/speechflow-cli/src/speechflow-node-xio-file.ts +93 -80
- package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +3 -2
- package/speechflow-cli/src/speechflow-node-xio-vban.ts +325 -0
- package/speechflow-cli/src/speechflow-node-xio-webrtc.ts +533 -0
- package/speechflow-cli/src/speechflow-node-xio-websocket.ts +2 -1
- package/speechflow-cli/src/speechflow-util-audio-wt.ts +4 -4
- package/speechflow-cli/src/speechflow-util-audio.ts +10 -10
- package/speechflow-cli/src/speechflow-util-error.ts +9 -7
- package/speechflow-cli/src/speechflow-util-llm.ts +367 -0
- package/speechflow-cli/src/speechflow-util-misc.ts +4 -4
- package/speechflow-cli/src/speechflow-util-queue.ts +4 -4
- package/speechflow-cli/src/speechflow-util-stream.ts +5 -3
- package/speechflow-cli/src/speechflow-util.ts +1 -0
- package/speechflow-ui-db/package.json +9 -9
- package/speechflow-ui-st/package.json +9 -9
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +0 -293
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +0 -1
- package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +0 -1
- package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +0 -1
- package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +0 -281
- package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +0 -247
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
+
** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* standard dependencies */
|
|
8
|
+
import EventEmitter from "node:events"
|
|
9
|
+
|
|
10
|
+
/* external dependencies */
|
|
11
|
+
import OpenAI from "openai"
|
|
12
|
+
import Anthropic from "@anthropic-ai/sdk"
|
|
13
|
+
import { GoogleGenAI } from "@google/genai"
|
|
14
|
+
import { Ollama, type ListResponse } from "ollama"
|
|
15
|
+
import * as Transformers from "@huggingface/transformers"
|
|
16
|
+
|
|
17
|
+
/* own utility types */
|
|
18
|
+
export type LLMCompleteMessage = {
|
|
19
|
+
role: "system" | "user" | "assistant"
|
|
20
|
+
content: string
|
|
21
|
+
}
|
|
22
|
+
export type LLMConfig = {
|
|
23
|
+
provider?: "openai" | "anthropic" | "google" | "ollama" | "transformers"
|
|
24
|
+
api?: string
|
|
25
|
+
model?: string
|
|
26
|
+
key?: string
|
|
27
|
+
timeout?: number
|
|
28
|
+
temperature?: number
|
|
29
|
+
maxTokens?: number
|
|
30
|
+
topP?: number
|
|
31
|
+
cacheDir?: string
|
|
32
|
+
}
|
|
33
|
+
export type LLMCompleteOptions = {
|
|
34
|
+
system?: string
|
|
35
|
+
messages?: LLMCompleteMessage[]
|
|
36
|
+
prompt: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* LLM class for unified LLM access */
|
|
40
|
+
export class LLM extends EventEmitter {
|
|
41
|
+
/* internal state */
|
|
42
|
+
private config: Required<LLMConfig>
|
|
43
|
+
private openai: OpenAI | null = null
|
|
44
|
+
private anthropic: Anthropic | null = null
|
|
45
|
+
private google: GoogleGenAI | null = null
|
|
46
|
+
private ollama: Ollama | null = null
|
|
47
|
+
private transformer: Transformers.TextGenerationPipeline | null = null
|
|
48
|
+
private initialized = false
|
|
49
|
+
|
|
50
|
+
/* construct LLM instance */
|
|
51
|
+
constructor (config: LLMConfig) {
|
|
52
|
+
/* pass-through to EventEmitter */
|
|
53
|
+
super()
|
|
54
|
+
|
|
55
|
+
/* provide configuration defaults */
|
|
56
|
+
this.config = {
|
|
57
|
+
provider: "openai",
|
|
58
|
+
api: "",
|
|
59
|
+
model: "",
|
|
60
|
+
key: "",
|
|
61
|
+
timeout: 30 * 1000,
|
|
62
|
+
temperature: 0.7,
|
|
63
|
+
maxTokens: 1024,
|
|
64
|
+
topP: 0.5,
|
|
65
|
+
cacheDir: "",
|
|
66
|
+
...config
|
|
67
|
+
} as Required<LLMConfig>
|
|
68
|
+
|
|
69
|
+
/* validate configuration options */
|
|
70
|
+
if (this.config.key === "") {
|
|
71
|
+
if (this.config.provider === "openai")
|
|
72
|
+
this.config.key = process.env.SPEECHFLOW_OPENAI_KEY ?? ""
|
|
73
|
+
else if (this.config.provider === "anthropic")
|
|
74
|
+
this.config.key = process.env.SPEECHFLOW_ANTHROPIC_KEY ?? ""
|
|
75
|
+
else if (this.config.provider === "google")
|
|
76
|
+
this.config.key = process.env.SPEECHFLOW_GOOGLE_KEY ?? ""
|
|
77
|
+
if (this.config.provider.match(/^(?:openai|anthropic|google)$/) && this.config.key === "")
|
|
78
|
+
throw new Error(`API key is required for provider "${this.config.provider}"`)
|
|
79
|
+
}
|
|
80
|
+
if (this.config.model === "")
|
|
81
|
+
throw new Error("model is required")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* internal logging helper */
|
|
85
|
+
private log (level: "info" | "warning" | "error", message: string): void {
|
|
86
|
+
this.emit("log", level, message)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* initialize the LLM client */
|
|
90
|
+
async open (): Promise<void> {
|
|
91
|
+
if (this.initialized)
|
|
92
|
+
return
|
|
93
|
+
if (this.config.provider === "openai") {
|
|
94
|
+
/* instantiate OpenAI API */
|
|
95
|
+
this.openai = new OpenAI({
|
|
96
|
+
...(this.config.api !== "" ? { baseURL: this.config.api } : {}),
|
|
97
|
+
apiKey: this.config.key,
|
|
98
|
+
timeout: this.config.timeout
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
else if (this.config.provider === "anthropic") {
|
|
102
|
+
/* instantiate Anthropic API */
|
|
103
|
+
this.anthropic = new Anthropic({
|
|
104
|
+
...(this.config.api !== "" ? { baseURL: this.config.api } : {}),
|
|
105
|
+
apiKey: this.config.key,
|
|
106
|
+
timeout: this.config.timeout
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
else if (this.config.provider === "google") {
|
|
110
|
+
/* instantiate Google API */
|
|
111
|
+
this.google = new GoogleGenAI({
|
|
112
|
+
apiKey: this.config.key,
|
|
113
|
+
httpOptions: {
|
|
114
|
+
timeout: this.config.timeout,
|
|
115
|
+
...(this.config.api !== "" ? { baseUrl: this.config.api } : {})
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
else if (this.config.provider === "ollama") {
|
|
120
|
+
/* instantiate Ollama API */
|
|
121
|
+
this.ollama = new Ollama({ host: this.config.api })
|
|
122
|
+
|
|
123
|
+
/* ensure the model is available */
|
|
124
|
+
let models: ListResponse
|
|
125
|
+
try {
|
|
126
|
+
models = await this.ollama.list()
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
throw new Error(`failed to connect to Ollama API at ${this.config.api}: ${err}`, { cause: err })
|
|
130
|
+
}
|
|
131
|
+
const exists = models.models.some((m) => m.name === this.config.model)
|
|
132
|
+
if (!exists) {
|
|
133
|
+
this.log("info", `LLM: model "${this.config.model}" still not present in Ollama -- ` +
|
|
134
|
+
"automatically downloading model")
|
|
135
|
+
let artifact = ""
|
|
136
|
+
let percent = 0
|
|
137
|
+
let lastLoggedPercent = -1
|
|
138
|
+
const interval = setInterval(() => {
|
|
139
|
+
if (percent !== lastLoggedPercent) {
|
|
140
|
+
this.log("info", `LLM: downloaded ${percent.toFixed(2)}% of artifact "${artifact}"`)
|
|
141
|
+
lastLoggedPercent = percent
|
|
142
|
+
}
|
|
143
|
+
}, 1000)
|
|
144
|
+
try {
|
|
145
|
+
const progress = await this.ollama.pull({ model: this.config.model, stream: true })
|
|
146
|
+
for await (const event of progress) {
|
|
147
|
+
if (event.digest)
|
|
148
|
+
artifact = event.digest
|
|
149
|
+
if (event.completed && event.total)
|
|
150
|
+
percent = (event.completed / event.total) * 100
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
clearInterval(interval)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (this.config.provider === "transformers") {
|
|
159
|
+
/* track download progress when instantiating Transformers pipeline */
|
|
160
|
+
const progressState = new Map<string, number>()
|
|
161
|
+
const progressCallback: Transformers.ProgressCallback = (progress: any) => {
|
|
162
|
+
let artifact = this.config.model
|
|
163
|
+
if (typeof progress.file === "string")
|
|
164
|
+
artifact += `:${progress.file}`
|
|
165
|
+
let percent = 0
|
|
166
|
+
if (typeof progress.loaded === "number" && typeof progress.total === "number")
|
|
167
|
+
percent = (progress.loaded / progress.total) * 100
|
|
168
|
+
else if (typeof progress.progress === "number")
|
|
169
|
+
percent = progress.progress
|
|
170
|
+
if (percent > 0)
|
|
171
|
+
progressState.set(artifact, percent)
|
|
172
|
+
}
|
|
173
|
+
const interval = setInterval(() => {
|
|
174
|
+
for (const [ artifact, percent ] of progressState) {
|
|
175
|
+
this.log("info", `LLM: downloaded ${percent.toFixed(2)}% of artifact "${artifact}"`)
|
|
176
|
+
if (percent >= 100.0)
|
|
177
|
+
progressState.delete(artifact)
|
|
178
|
+
}
|
|
179
|
+
}, 1000)
|
|
180
|
+
|
|
181
|
+
/* instantiate HuggingFace Transformers text generation pipeline */
|
|
182
|
+
try {
|
|
183
|
+
const pipelinePromise = Transformers.pipeline("text-generation", this.config.model, {
|
|
184
|
+
...(this.config.cacheDir !== "" ? { cache_dir: this.config.cacheDir } : {}),
|
|
185
|
+
dtype: "q4",
|
|
186
|
+
device: "auto",
|
|
187
|
+
progress_callback: progressCallback
|
|
188
|
+
})
|
|
189
|
+
this.transformer = await pipelinePromise
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
throw new Error(`failed to instantiate HuggingFace Transformers pipeline: ${err}`, { cause: err })
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
clearInterval(interval)
|
|
196
|
+
}
|
|
197
|
+
if (this.transformer === null)
|
|
198
|
+
throw new Error("failed to instantiate HuggingFace Transformers pipeline")
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
const exhaustive: never = this.config.provider
|
|
202
|
+
throw new Error(`unsupported LLM provider: ${exhaustive}`)
|
|
203
|
+
}
|
|
204
|
+
this.log("info", `LLM: initialized ${this.config.provider} client ` +
|
|
205
|
+
`(${this.config.api !== "" ? `api: ${this.config.api}, ` : ""}model: ${this.config.model})`)
|
|
206
|
+
this.initialized = true
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* perform a completion */
|
|
210
|
+
async complete (options: LLMCompleteOptions): Promise<string> {
|
|
211
|
+
if (!this.initialized)
|
|
212
|
+
throw new Error("LLM still not initialized")
|
|
213
|
+
|
|
214
|
+
/* build messages array */
|
|
215
|
+
const messages: LLMCompleteMessage[] = []
|
|
216
|
+
if (options.system)
|
|
217
|
+
messages.push({ role: "system", content: options.system })
|
|
218
|
+
if (options.messages)
|
|
219
|
+
messages.push(...options.messages)
|
|
220
|
+
messages.push({ role: "user", content: options.prompt })
|
|
221
|
+
|
|
222
|
+
/* perform LLM query */
|
|
223
|
+
if (this.config.provider === "openai") {
|
|
224
|
+
if (!this.openai)
|
|
225
|
+
throw new Error("OpenAI client not available")
|
|
226
|
+
|
|
227
|
+
/* perform OpenAI chat completion */
|
|
228
|
+
const completion = await this.openai.chat.completions.create({
|
|
229
|
+
model: this.config.model,
|
|
230
|
+
max_tokens: this.config.maxTokens,
|
|
231
|
+
temperature: this.config.temperature,
|
|
232
|
+
top_p: this.config.topP,
|
|
233
|
+
messages: messages as OpenAI.ChatCompletionMessageParam[]
|
|
234
|
+
}).catch((err) => {
|
|
235
|
+
throw new Error(`failed to perform OpenAI chat completion: ${err}`, { cause: err })
|
|
236
|
+
})
|
|
237
|
+
const content = completion?.choices?.[0]?.message?.content
|
|
238
|
+
if (!content)
|
|
239
|
+
throw new Error("OpenAI API returned empty content")
|
|
240
|
+
return content
|
|
241
|
+
}
|
|
242
|
+
else if (this.config.provider === "anthropic") {
|
|
243
|
+
if (!this.anthropic)
|
|
244
|
+
throw new Error("Anthropic client not available")
|
|
245
|
+
|
|
246
|
+
/* separate system message from other messages for Anthropic API */
|
|
247
|
+
const systemMessage = messages.find((m) => m.role === "system")
|
|
248
|
+
const chatMessages = messages.filter((m) => m.role !== "system")
|
|
249
|
+
|
|
250
|
+
/* perform Anthropic chat completion */
|
|
251
|
+
const message = await this.anthropic.messages.create({
|
|
252
|
+
model: this.config.model,
|
|
253
|
+
max_tokens: this.config.maxTokens,
|
|
254
|
+
temperature: this.config.temperature,
|
|
255
|
+
top_p: this.config.topP,
|
|
256
|
+
system: systemMessage?.content,
|
|
257
|
+
messages: chatMessages as Anthropic.MessageParam[]
|
|
258
|
+
}).catch((err) => {
|
|
259
|
+
throw new Error(`failed to perform Anthropic chat completion: ${err}`, { cause: err })
|
|
260
|
+
})
|
|
261
|
+
const content = message?.content?.[0]
|
|
262
|
+
if (!content || content.type !== "text")
|
|
263
|
+
throw new Error("Anthropic API returned empty or non-text content")
|
|
264
|
+
return content.text
|
|
265
|
+
}
|
|
266
|
+
else if (this.config.provider === "google") {
|
|
267
|
+
if (!this.google)
|
|
268
|
+
throw new Error("Google client not available")
|
|
269
|
+
|
|
270
|
+
/* convert messages for Google API */
|
|
271
|
+
const systemInstruction =
|
|
272
|
+
messages.find((m) => m.role === "system")?.content
|
|
273
|
+
const contents =
|
|
274
|
+
messages.filter((m) => m.role !== "system").map((m) => ({
|
|
275
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
276
|
+
parts: [ { text: m.content } ]
|
|
277
|
+
}))
|
|
278
|
+
|
|
279
|
+
/* perform Google chat completion */
|
|
280
|
+
const response = await this.google.models.generateContent({
|
|
281
|
+
model: this.config.model,
|
|
282
|
+
contents,
|
|
283
|
+
config: {
|
|
284
|
+
maxOutputTokens: this.config.maxTokens,
|
|
285
|
+
temperature: this.config.temperature,
|
|
286
|
+
topP: this.config.topP,
|
|
287
|
+
...(systemInstruction ? { systemInstruction } : {})
|
|
288
|
+
}
|
|
289
|
+
}).catch((err) => {
|
|
290
|
+
throw new Error(`failed to perform Google chat completion: ${err}`, { cause: err })
|
|
291
|
+
})
|
|
292
|
+
const content = response?.text
|
|
293
|
+
if (!content)
|
|
294
|
+
throw new Error("Google API returned empty content")
|
|
295
|
+
return content
|
|
296
|
+
}
|
|
297
|
+
else if (this.config.provider === "ollama") {
|
|
298
|
+
if (!this.ollama)
|
|
299
|
+
throw new Error("Ollama client not available")
|
|
300
|
+
|
|
301
|
+
/* perform Ollama chat completion */
|
|
302
|
+
const response = await this.ollama.chat({
|
|
303
|
+
model: this.config.model,
|
|
304
|
+
messages,
|
|
305
|
+
keep_alive: "10m",
|
|
306
|
+
options: {
|
|
307
|
+
num_predict: this.config.maxTokens,
|
|
308
|
+
temperature: this.config.temperature,
|
|
309
|
+
top_p: this.config.topP
|
|
310
|
+
}
|
|
311
|
+
}).catch((err) => {
|
|
312
|
+
throw new Error(`failed to perform Ollama chat completion: ${err}`, { cause: err })
|
|
313
|
+
})
|
|
314
|
+
const content = response?.message?.content
|
|
315
|
+
if (!content)
|
|
316
|
+
throw new Error("Ollama API returned empty content")
|
|
317
|
+
return content
|
|
318
|
+
}
|
|
319
|
+
else if (this.config.provider === "transformers") {
|
|
320
|
+
if (!this.transformer)
|
|
321
|
+
throw new Error("HuggingFace Transformers pipeline not available")
|
|
322
|
+
|
|
323
|
+
/* perform HuggingFace Transformers text generation */
|
|
324
|
+
const result = await this.transformer(messages, {
|
|
325
|
+
max_new_tokens: this.config.maxTokens,
|
|
326
|
+
temperature: this.config.temperature,
|
|
327
|
+
top_p: this.config.topP,
|
|
328
|
+
do_sample: true
|
|
329
|
+
}).catch((err) => {
|
|
330
|
+
throw new Error(`failed to perform HuggingFace Transformers text generation: ${err}`, { cause: err })
|
|
331
|
+
})
|
|
332
|
+
const single = Array.isArray(result) ? result[0] : result
|
|
333
|
+
const generatedText = (single as Transformers.TextGenerationSingle).generated_text
|
|
334
|
+
const content = typeof generatedText === "string" ?
|
|
335
|
+
generatedText :
|
|
336
|
+
generatedText.at(-1)?.content
|
|
337
|
+
if (!content)
|
|
338
|
+
throw new Error("HuggingFace Transformers API returned empty content")
|
|
339
|
+
return content
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
const exhaustive: never = this.config.provider
|
|
343
|
+
throw new Error(`unsupported LLM provider: ${exhaustive}`)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* close the LLM client */
|
|
348
|
+
async close (): Promise<void> {
|
|
349
|
+
if (!this.initialized)
|
|
350
|
+
return
|
|
351
|
+
if (this.config.provider === "openai")
|
|
352
|
+
this.openai = null
|
|
353
|
+
else if (this.config.provider === "anthropic")
|
|
354
|
+
this.anthropic = null
|
|
355
|
+
else if (this.config.provider === "google")
|
|
356
|
+
this.google = null
|
|
357
|
+
else if (this.config.provider === "ollama") {
|
|
358
|
+
this.ollama?.abort()
|
|
359
|
+
this.ollama = null
|
|
360
|
+
}
|
|
361
|
+
else if (this.config.provider === "transformers") {
|
|
362
|
+
this.transformer?.dispose()
|
|
363
|
+
this.transformer = null
|
|
364
|
+
}
|
|
365
|
+
this.initialized = false
|
|
366
|
+
}
|
|
367
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
/* sleep: wait a duration of time and then resolve */
|
|
8
8
|
export function sleep (durationMs: number) {
|
|
9
|
-
return new Promise<void>((resolve
|
|
9
|
+
return new Promise<void>((resolve) => {
|
|
10
10
|
setTimeout(() => {
|
|
11
11
|
resolve()
|
|
12
12
|
}, durationMs)
|
|
@@ -14,10 +14,10 @@ export function sleep (durationMs: number) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/* timeout: wait a duration of time and then reject */
|
|
17
|
-
export function timeout (durationMs: number) {
|
|
18
|
-
return new Promise<never>((
|
|
17
|
+
export function timeout (durationMs: number, info = "timeout") {
|
|
18
|
+
return new Promise<never>((_resolve, reject) => {
|
|
19
19
|
setTimeout(() => {
|
|
20
|
-
reject(new Error(
|
|
20
|
+
reject(new Error(info))
|
|
21
21
|
}, durationMs)
|
|
22
22
|
})
|
|
23
23
|
}
|
|
@@ -34,7 +34,7 @@ export class SingleQueue<T> extends EventEmitter {
|
|
|
34
34
|
this.emit("dequeue")
|
|
35
35
|
}
|
|
36
36
|
read () {
|
|
37
|
-
return new Promise<T>((resolve
|
|
37
|
+
return new Promise<T>((resolve) => {
|
|
38
38
|
const tryToConsume = () => {
|
|
39
39
|
const item = this.queue.pop()
|
|
40
40
|
if (item !== undefined)
|
|
@@ -69,7 +69,7 @@ export class DoubleQueue<T0, T1> extends EventEmitter {
|
|
|
69
69
|
this.notify()
|
|
70
70
|
}
|
|
71
71
|
read () {
|
|
72
|
-
return new Promise<[ T0, T1 ]>((resolve
|
|
72
|
+
return new Promise<[ T0, T1 ]>((resolve) => {
|
|
73
73
|
const consume = (): [ T0, T1 ] | undefined => {
|
|
74
74
|
if (this.queue0.length > 0 && this.queue1.length > 0) {
|
|
75
75
|
const item0 = this.queue0.pop() as T0
|
|
@@ -90,7 +90,7 @@ export class DoubleQueue<T0, T1> extends EventEmitter {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/* queue element
|
|
93
|
+
/* queue element */
|
|
94
94
|
export type QueueElement = { type: string }
|
|
95
95
|
|
|
96
96
|
/* queue pointer */
|
|
@@ -228,7 +228,7 @@ export class Queue<T extends QueueElement> extends EventEmitter {
|
|
|
228
228
|
}
|
|
229
229
|
pointerDelete (name: string): void {
|
|
230
230
|
if (!this.pointers.has(name))
|
|
231
|
-
throw new Error("pointer not
|
|
231
|
+
throw new Error("pointer does not exist")
|
|
232
232
|
this.pointers.delete(name)
|
|
233
233
|
}
|
|
234
234
|
trim (): void {
|
|
@@ -204,7 +204,7 @@ export class StreamWrapper extends Stream.Transform {
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
/* helper function for destruction of a stream */
|
|
207
|
-
export async function destroyStream(
|
|
207
|
+
export async function destroyStream (
|
|
208
208
|
stream: Stream.Readable | Stream.Writable | Stream.Duplex | Stream.Transform
|
|
209
209
|
) {
|
|
210
210
|
/* signal the end for a writable stream */
|
|
@@ -217,8 +217,10 @@ export async function destroyStream(
|
|
|
217
217
|
new Promise<void>((resolve) => {
|
|
218
218
|
stream.end(() => { resolve() })
|
|
219
219
|
}),
|
|
220
|
-
util.
|
|
221
|
-
])
|
|
220
|
+
util.timeout(5000, "stream end timeout")
|
|
221
|
+
]).catch(() => {
|
|
222
|
+
/* ignore timeout -- stream will be destroyed anyway */
|
|
223
|
+
})
|
|
222
224
|
|
|
223
225
|
/* destroy the stream */
|
|
224
226
|
stream.destroy()
|
|
@@ -18,17 +18,17 @@
|
|
|
18
18
|
"luxon": "3.7.2",
|
|
19
19
|
"@opensumi/reconnecting-websocket": "4.4.0",
|
|
20
20
|
"axios": "1.13.2",
|
|
21
|
-
"typopro-web": "4.2.
|
|
21
|
+
"typopro-web": "4.2.8",
|
|
22
22
|
"@fortawesome/fontawesome-free": "7.1.0",
|
|
23
23
|
"patch-package": "8.0.1",
|
|
24
24
|
"@rse/stx": "1.1.2"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"vite": "7.2.
|
|
28
|
-
"typescript-eslint": "8.
|
|
29
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
30
|
-
"@typescript-eslint/parser": "8.
|
|
31
|
-
"@vitejs/plugin-vue": "6.0.
|
|
27
|
+
"vite": "7.2.7",
|
|
28
|
+
"typescript-eslint": "8.49.0",
|
|
29
|
+
"@typescript-eslint/eslint-plugin": "8.49.0",
|
|
30
|
+
"@typescript-eslint/parser": "8.49.0",
|
|
31
|
+
"@vitejs/plugin-vue": "6.0.3",
|
|
32
32
|
"@rollup/plugin-yaml": "4.1.2",
|
|
33
33
|
"vite-plugin-node-polyfills": "0.24.0",
|
|
34
34
|
"vite-svg-loader": "5.1.0",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
|
|
38
38
|
"@vue/eslint-config-typescript": "14.6.0",
|
|
39
39
|
"vue-eslint-parser": "10.2.0",
|
|
40
|
-
"eslint": "9.39.
|
|
41
|
-
"@eslint/js": "9.39.
|
|
40
|
+
"eslint": "9.39.2",
|
|
41
|
+
"@eslint/js": "9.39.2",
|
|
42
42
|
"neostandard": "0.12.2",
|
|
43
43
|
"eslint-plugin-import": "2.32.0",
|
|
44
44
|
"eslint-plugin-vue": "10.6.2",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"postcss-html": "1.8.0",
|
|
59
59
|
"stylus": "0.64.0",
|
|
60
60
|
"typescript": "5.9.3",
|
|
61
|
-
"vue-tsc": "3.1.
|
|
61
|
+
"vue-tsc": "3.1.8",
|
|
62
62
|
"delay-cli": "3.0.0",
|
|
63
63
|
"cross-env": "10.1.0",
|
|
64
64
|
"serve": "14.2.5",
|
|
@@ -19,18 +19,18 @@
|
|
|
19
19
|
"luxon": "3.7.2",
|
|
20
20
|
"@opensumi/reconnecting-websocket": "4.4.0",
|
|
21
21
|
"axios": "1.13.2",
|
|
22
|
-
"typopro-web": "4.2.
|
|
22
|
+
"typopro-web": "4.2.8",
|
|
23
23
|
"@fortawesome/fontawesome-free": "7.1.0",
|
|
24
24
|
"patch-package": "8.0.1",
|
|
25
25
|
"@rse/stx": "1.1.2",
|
|
26
26
|
"animejs": "4.2.2"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"vite": "7.2.
|
|
30
|
-
"typescript-eslint": "8.
|
|
31
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
32
|
-
"@typescript-eslint/parser": "8.
|
|
33
|
-
"@vitejs/plugin-vue": "6.0.
|
|
29
|
+
"vite": "7.2.7",
|
|
30
|
+
"typescript-eslint": "8.49.0",
|
|
31
|
+
"@typescript-eslint/eslint-plugin": "8.49.0",
|
|
32
|
+
"@typescript-eslint/parser": "8.49.0",
|
|
33
|
+
"@vitejs/plugin-vue": "6.0.3",
|
|
34
34
|
"@rollup/plugin-yaml": "4.1.2",
|
|
35
35
|
"vite-plugin-node-polyfills": "0.24.0",
|
|
36
36
|
"vite-svg-loader": "5.1.0",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
|
|
40
40
|
"@vue/eslint-config-typescript": "14.6.0",
|
|
41
41
|
"vue-eslint-parser": "10.2.0",
|
|
42
|
-
"eslint": "9.39.
|
|
43
|
-
"@eslint/js": "9.39.
|
|
42
|
+
"eslint": "9.39.2",
|
|
43
|
+
"@eslint/js": "9.39.2",
|
|
44
44
|
"neostandard": "0.12.2",
|
|
45
45
|
"eslint-plugin-import": "2.32.0",
|
|
46
46
|
"eslint-plugin-vue": "10.6.2",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"postcss-html": "1.8.0",
|
|
61
61
|
"stylus": "0.64.0",
|
|
62
62
|
"typescript": "5.9.3",
|
|
63
|
-
"vue-tsc": "3.1.
|
|
63
|
+
"vue-tsc": "3.1.8",
|
|
64
64
|
"delay-cli": "3.0.0",
|
|
65
65
|
"cross-env": "10.1.0",
|
|
66
66
|
"serve": "14.2.5",
|