speechflow 1.6.3 → 1.6.4

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.
Files changed (43) hide show
  1. package/.claude/settings.local.json +3 -0
  2. package/CHANGELOG.md +10 -0
  3. package/README.md +82 -45
  4. package/etc/speechflow.yaml +19 -14
  5. package/package.json +5 -5
  6. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +2 -1
  7. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
  8. package/speechflow-cli/dst/speechflow-node-a2a-expander.js +2 -1
  9. package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
  10. package/speechflow-cli/dst/speechflow-node-a2a-gender.js +4 -14
  11. package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
  12. package/speechflow-cli/dst/speechflow-node-a2a-meter.js +11 -8
  13. package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
  14. package/speechflow-cli/dst/speechflow-node-x2x-trace.d.ts +1 -0
  15. package/speechflow-cli/dst/speechflow-node-x2x-trace.js +22 -2
  16. package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
  17. package/speechflow-cli/etc/stx.conf +6 -10
  18. package/speechflow-cli/package.json +13 -13
  19. package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +2 -1
  20. package/speechflow-cli/src/speechflow-node-a2a-expander.ts +2 -1
  21. package/speechflow-cli/src/speechflow-node-a2a-meter.ts +11 -8
  22. package/speechflow-cli/src/speechflow-node-x2x-trace.ts +25 -2
  23. package/speechflow-ui-db/dst/index.js +24 -33
  24. package/speechflow-ui-db/package.json +12 -10
  25. package/speechflow-ui-db/src/app.vue +16 -2
  26. package/speechflow-ui-st/.claude/settings.local.json +3 -0
  27. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-BoldIt.eot +0 -0
  28. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-BoldIt.ttf +0 -0
  29. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-BoldIt.woff +0 -0
  30. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-RegularIt.eot +0 -0
  31. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-RegularIt.ttf +0 -0
  32. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-RegularIt.woff +0 -0
  33. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-Semibold.eot +0 -0
  34. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-Semibold.ttf +0 -0
  35. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-Semibold.woff +0 -0
  36. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-SemiboldIt.eot +0 -0
  37. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-SemiboldIt.ttf +0 -0
  38. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-SemiboldIt.woff +0 -0
  39. package/speechflow-ui-st/dst/index.css +2 -2
  40. package/speechflow-ui-st/dst/index.js +26 -28
  41. package/speechflow-ui-st/package.json +11 -10
  42. package/speechflow-ui-st/src/app.vue +142 -48
  43. package/speechflow-ui-st/src/index.ts +4 -0
@@ -16,20 +16,20 @@
16
16
  "dependencies": {
17
17
  "vue": "3.5.21",
18
18
  "vue3-spinners": "1.3.1",
19
- "moment": "2.30.1",
20
19
  "luxon": "3.7.2",
21
20
  "@opensumi/reconnecting-websocket": "4.4.0",
22
- "axios": "1.11.0",
21
+ "axios": "1.12.1",
23
22
  "typopro-web": "4.2.7",
24
23
  "@fortawesome/fontawesome-free": "7.0.1",
25
24
  "patch-package": "8.0.0",
26
- "@rse/stx": "1.1.0"
25
+ "@rse/stx": "1.1.1",
26
+ "animejs": "4.1.3"
27
27
  },
28
28
  "devDependencies": {
29
- "vite": "7.1.4",
30
- "typescript-eslint": "8.42.0",
31
- "@typescript-eslint/eslint-plugin": "8.42.0",
32
- "@typescript-eslint/parser": "8.42.0",
29
+ "vite": "7.1.5",
30
+ "typescript-eslint": "8.43.0",
31
+ "@typescript-eslint/eslint-plugin": "8.43.0",
32
+ "@typescript-eslint/parser": "8.43.0",
33
33
  "@vitejs/plugin-vue": "6.0.1",
34
34
  "@rollup/plugin-yaml": "4.1.2",
35
35
  "vite-plugin-node-polyfills": "0.24.0",
@@ -50,7 +50,7 @@
50
50
 
51
51
  "nodemon": "3.1.10",
52
52
  "shx": "0.4.0",
53
- "stylelint": "16.23.1",
53
+ "stylelint": "16.24.0",
54
54
  "stylelint-config-html": "1.1.0",
55
55
  "stylelint-config-recommended-vue": "1.6.1",
56
56
  "stylelint-config-standard": "39.0.0",
@@ -59,12 +59,13 @@
59
59
  "postcss-html": "1.8.0",
60
60
  "stylus": "0.64.0",
61
61
  "typescript": "5.9.2",
62
- "vue-tsc": "3.0.6",
62
+ "vue-tsc": "3.0.7",
63
63
  "delay-cli": "2.0.0",
64
64
  "cross-env": "10.0.0",
65
65
  "serve": "14.2.5",
66
66
 
67
- "@types/luxon": "3.7.1"
67
+ "@types/luxon": "3.7.1",
68
+ "@types/animejs": "3.1.13"
68
69
  },
69
70
  "overrides": {
70
71
  "@liuli-util/vite-plugin-node": { "vite": ">=6.0.0" }
@@ -8,23 +8,25 @@
8
8
 
9
9
  <template>
10
10
  <div class="app">
11
- <div class="block">
12
- <div class="text-col"
13
- v-bind:class="{ intermediate: lastTextBlockKind === 'intermediate' }">
14
- <div v-bind:key="value"
15
- v-for="(value, idx) of text"
16
- class="text-value">
17
- {{ value }}
18
- <span class="cursor" v-if="idx === (text.length - 1) && lastTextBlockKind === 'intermediate'">
19
- <spinner-grid class="spinner-grid" size="32"/>
11
+ <div class="area">
12
+ <div class="block" ref="block">
13
+ <span v-bind:key="chunk.id"
14
+ v-for="(chunk, idx) of text"
15
+ class="chunk"
16
+ v-bind:class="{ intermediate: chunk.kind === 'intermediate', removed: chunk.removed }"
17
+ v-bind:ref="`chunk-${chunk.id}`">
18
+ {{ chunk.text }}
19
+ <span class="cursor" v-if="idx === (text.length - 1) && chunk.kind === 'intermediate'">
20
+ <spinner-grid class="spinner-grid" size="50"/>
20
21
  </span>
21
- </div>
22
+ </span>
22
23
  </div>
23
24
  </div>
24
25
  </div>
25
26
  </template>
26
27
 
27
28
  <style lang="stylus">
29
+ /* entire app */
28
30
  .app
29
31
  position: relative
30
32
  width: 100vw
@@ -35,53 +37,74 @@
35
37
  flex-direction: column
36
38
  justify-content: center
37
39
  align-items: center
38
- .block
39
- height: 20vh
40
- width: 80vw
40
+
41
+ /* rendering area */
42
+ .area
43
+ height: auto
44
+ min-height: 30vh
45
+ max-height: 60vh
46
+ width: 75vw
41
47
  position: absolute
42
- bottom: 1vh
43
- .text-col
48
+ bottom: 4vh
49
+ mask: linear-gradient(to bottom, transparent 0%, black 50%)
50
+ overflow-x: hidden
51
+ overflow-y: hidden
52
+ display: flex
53
+ flex-direction: column
54
+ align-items: center
55
+ justify-content: flex-end
56
+
57
+ /* content block */
58
+ .block
59
+ display: block
60
+ text-align: left
44
61
  width: 100%
45
- height: 100%
46
- overflow-x: hidden
47
- overflow-y: scroll
48
- display: flex
49
- flex-direction: column
50
- align-items: flex-end
51
- justify-content: flex-end
52
- .text-value
53
- width: calc(100% - 2 * 1.0vw)
54
- background-color: #000000c0
55
- color: #ffffff
56
- border-radius: 1vw
57
- font-size: 2vw
58
- padding: 0.5vw 1.0vw
59
- margin-bottom: 0.5vw
60
- overflow-wrap: break-word
62
+ overflow-wrap: break-word
63
+
64
+ /* content chunk */
65
+ .chunk
66
+ color: #eeeeee
67
+ background-color: #000000b0
68
+ text-shadow: 0 0 0.20vw #000000
69
+ -webkit-text-stroke: 0.05vw #000000
70
+ font-size: 2.0vw
71
+ font-weight: 600
72
+ line-height: 1.4
73
+ padding-left: 0.20vw
74
+ padding-right: 0.20vw
61
75
  .cursor
62
76
  display: inline-block
63
77
  margin-left: 10px
64
- .text-col.intermediate
65
- .text-value:last-child
66
- background-color: #666666c0
78
+ &.intermediate:last-child
79
+ color: #ffffff
80
+ &.removed
81
+ opacity: 0
67
82
  </style>
68
83
 
69
84
  <script setup lang="ts">
70
- import { defineComponent } from "vue"
71
- import { VueSpinnerGrid } from "vue3-spinners"
72
- import moment from "moment"
73
- import { Duration } from "luxon"
74
- import ReconnectingWebSocket from "@opensumi/reconnecting-websocket"
85
+ import { defineComponent } from "vue"
86
+ import { VueSpinnerGrid } from "vue3-spinners"
87
+ import { DateTime, Duration } from "luxon"
88
+ import ReconnectingWebSocket from "@opensumi/reconnecting-websocket"
89
+ import * as anime from "animejs"
75
90
  </script>
76
91
 
77
92
  <script lang="ts">
93
+ type TextChunk = {
94
+ id: string,
95
+ timestamp: DateTime,
96
+ kind: "intermediate" | "final",
97
+ text: string,
98
+ removing: boolean,
99
+ removed: boolean
100
+ }
78
101
  type SpeechFlowChunk = {
79
102
  timestampStart: Duration,
80
103
  timestampEnd: Duration,
81
104
  kind: "intermediate" | "final",
82
105
  type: "text",
83
106
  payload: string,
84
- meta: Map<string, any>
107
+ meta: Map<string, unknown>
85
108
  }
86
109
  export default defineComponent({
87
110
  name: "app",
@@ -89,13 +112,39 @@ export default defineComponent({
89
112
  "spinner-grid": VueSpinnerGrid
90
113
  },
91
114
  data: () => ({
92
- text: [] as string[],
93
- lastTextBlockKind: ""
115
+ text: [] as TextChunk[],
116
+ chunkIdCounter: 0,
117
+ cleanupIntervalId: null as ReturnType<typeof setInterval> | null
94
118
  }),
95
119
  async mounted () {
96
120
  /* determine API URL */
97
121
  const url = new URL("/api", document.location.href).toString()
98
122
 
123
+ /* optically remove outdated text chunks */
124
+ this.cleanupIntervalId = setInterval(() => {
125
+ for (const chunk of this.text) {
126
+ if (chunk.timestamp < DateTime.now().minus({ seconds: 20 }) && !chunk.removing && !chunk.removed) {
127
+ const el = this.$refs[`chunk-${chunk.id}`] as HTMLSpanElement
128
+ if (!el)
129
+ continue
130
+
131
+ /* start removing */
132
+ chunk.removing = true
133
+ chunk.removed = false
134
+ anime.animate(el, {
135
+ opacity: [ 1, 0 ],
136
+ duration: 2000,
137
+ easing: "easeOutQuad",
138
+ onComplete: () => {
139
+ /* end removing */
140
+ chunk.removing = false
141
+ chunk.removed = true
142
+ }
143
+ })
144
+ }
145
+ }
146
+ }, 500)
147
+
99
148
  /* connect to WebSocket API for receiving dashboard information */
100
149
  const ws = new ReconnectingWebSocket(url, [], {
101
150
  reconnectionDelayGrowFactor: 1.3,
@@ -104,7 +153,18 @@ export default defineComponent({
104
153
  connectionTimeout: 4000,
105
154
  minUptime: 5000
106
155
  })
156
+
157
+ /* track connection open/close */
158
+ ws.addEventListener("open", (ev) => {
159
+ this.log("INFO", "WebSocket connection established")
160
+ })
161
+ ws.addEventListener("close", (ev) => {
162
+ this.log("INFO", "WebSocket connection destroyed")
163
+ })
164
+
165
+ /* receive messages */
107
166
  ws.addEventListener("message", (ev) => {
167
+ /* parse message */
108
168
  let chunk: SpeechFlowChunk
109
169
  try {
110
170
  chunk = JSON.parse(ev.data)
@@ -113,18 +173,52 @@ export default defineComponent({
113
173
  this.log("ERROR", "Failed to parse WebSocket message", { error, data: ev.data })
114
174
  return
115
175
  }
116
- if (this.lastTextBlockKind === "intermediate")
117
- this.text[this.text.length - 1] = chunk.payload
176
+
177
+ /* process text chunks */
178
+ if (this.text.length > 0 && this.text[this.text.length - 1].kind === "intermediate") {
179
+ /* override previous intermediate text chunk
180
+ with either another intermediate one or a final one */
181
+ const lastChunk = this.text[this.text.length - 1]
182
+ lastChunk.text = chunk.payload
183
+ lastChunk.kind = chunk.kind
184
+ lastChunk.timestamp = DateTime.now()
185
+ }
118
186
  else {
119
- this.text.push(chunk.payload)
120
- this.text = this.text.slice(-2)
187
+ /* remove content in case all chunks were removed
188
+ (this way new next chunk starts at the left edge again
189
+ and the overall memory consumption is reduced) */
190
+ if (this.text.every((chunk) => chunk.removed))
191
+ this.text = []
192
+
193
+ /* add new text chunk */
194
+ this.text.push({
195
+ id: `chunk-${this.chunkIdCounter++}`,
196
+ text: chunk.payload,
197
+ kind: chunk.kind,
198
+ timestamp: DateTime.now(),
199
+ removing: false,
200
+ removed: false
201
+ })
121
202
  }
122
- this.lastTextBlockKind = chunk.kind
203
+
204
+ /* ensure we always scrolled the chunks inside the block to the bottom */
205
+ this.$nextTick(() => {
206
+ const block = this.$refs.block as HTMLDivElement
207
+ block.scrollTop = block.scrollHeight
208
+ })
123
209
  })
124
210
  },
211
+ beforeUnmount () {
212
+ /* cleanup */
213
+ if (this.cleanupIntervalId !== null) {
214
+ clearInterval(this.cleanupIntervalId)
215
+ this.cleanupIntervalId = null
216
+ }
217
+ },
125
218
  methods: {
219
+ /* helper function for console logging */
126
220
  log (level: string, msg: string, data: { [ key: string ]: any } | null = null) {
127
- const timestamp = moment().format("YYYY-MM-DD hh:mm:ss.SSS")
221
+ const timestamp = DateTime.now().toFormat("yyyy-MM-dd HH:mm:ss.SSS")
128
222
  let output = `${timestamp} [${level}]: ${msg}`
129
223
  if (data !== null)
130
224
  output += ` (${Object.keys(data)
@@ -7,7 +7,11 @@
7
7
  /* external dependencies */
8
8
  import * as Vue from "vue"
9
9
  import "typopro-web/web/TypoPRO-SourceSansPro/TypoPRO-SourceSansPro-Regular.css"
10
+ import "typopro-web/web/TypoPRO-SourceSansPro/TypoPRO-SourceSansPro-RegularIt.css"
11
+ import "typopro-web/web/TypoPRO-SourceSansPro/TypoPRO-SourceSansPro-Semibold.css"
12
+ import "typopro-web/web/TypoPRO-SourceSansPro/TypoPRO-SourceSansPro-SemiboldIt.css"
10
13
  import "typopro-web/web/TypoPRO-SourceSansPro/TypoPRO-SourceSansPro-Bold.css"
14
+ import "typopro-web/web/TypoPRO-SourceSansPro/TypoPRO-SourceSansPro-BoldIt.css"
11
15
  import "@fortawesome/fontawesome-free/js/all.min.js"
12
16
  import "@fortawesome/fontawesome-free/css/all.min.css"
13
17