Skip to content

Commit f9ef918

Browse files
committed
impl stable sentence
1 parent 0b7801c commit f9ef918

16 files changed

Lines changed: 594 additions & 91 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ dist
77

88
asr-server
99
nlp-server
10-
/config.json
10+
/config.json
11+
subtitle

package-lock.json

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"dependencies": {
3535
"ave-react": "^0.1.4",
3636
"axios": "^1.3.2",
37-
"debounce": "^1.2.1"
37+
"debounce": "^1.2.1",
38+
"sentence-splitter": "^4.2.0"
3839
}
3940
}

src/app.tsx

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import React, { useCallback, useEffect, useMemo } from "react";
2-
import { AveRenderer, Grid, Window, getAppContext, IIconResource, IWindowComponentProps, Button, CheckBox, ICheckBoxComponentProps } from "ave-react";
1+
import React, { useCallback, useEffect, useMemo, useState } from "react";
2+
import { AveRenderer, Grid, Window, getAppContext, IIconResource, IWindowComponentProps, Button, CheckBox, ICheckBoxComponentProps, ScrollBar, Label, IScrollBarComponentProps } from "ave-react";
33
import { App, ThemePredefined_Dark, CheckValue } from "ave-ui";
44
import { VoskAsrEngine } from "./asr";
55
import { HelsinkiNlpEngine } from "./nlp";
66
import { containerLayout, controlLayout } from "./layout";
77
import { iconResource } from "./resource";
8-
import { onMeasure, onTranslate, shadowRelated } from "./shadow";
8+
import { logger, onMeasure, onTranslate, shadowRelated } from "./shadow";
99
import { getAsrConfig, getNlpConfig } from "./config";
1010

1111
function onInit(app: App) {
@@ -20,6 +20,14 @@ function initTheme() {
2020
themeDark.SetStyle(themeImage, 0);
2121
}
2222

23+
enum ButtonText {
24+
Measure = "设置字幕区",
25+
Recognize = "语音识别",
26+
SetTopMost = "字幕置顶",
27+
SubtitleEn = "英文字幕",
28+
SubtitleZh = "中文字幕",
29+
}
30+
2331
export function Echo() {
2432
const asrEngine = useMemo(
2533
() =>
@@ -57,29 +65,39 @@ export function Echo() {
5765
}, []);
5866

5967
const onSetRecognize = useCallback<ICheckBoxComponentProps["onCheck"]>((sender) => {
68+
shadowRelated.subtitleQueue = [];
69+
6070
let shouldRecognize = false;
6171

6272
const checkValue = sender.GetValue();
6373
if (checkValue === CheckValue.Unchecked) {
6474
shouldRecognize = false;
75+
logger.end();
6576
} else if (checkValue === CheckValue.Checked) {
6677
shouldRecognize = true;
78+
logger.start();
6779
}
6880

6981
shadowRelated.shouldRecognize = shouldRecognize;
7082
}, []);
7183

72-
const onSetPunct = useCallback<ICheckBoxComponentProps["onCheck"]>((sender) => {
73-
let shouldResotrePunct = false;
74-
84+
const onSetDisplaySubtitle = useCallback<ICheckBoxComponentProps["onCheck"]>((sender) => {
7585
const checkValue = sender.GetValue();
76-
if (checkValue === CheckValue.Unchecked) {
77-
shouldResotrePunct = false;
78-
} else if (checkValue === CheckValue.Checked) {
79-
shouldResotrePunct = true;
86+
const text = sender.GetText();
87+
const isChecked = checkValue === CheckValue.Checked;
88+
if (text === ButtonText.SubtitleEn) {
89+
shadowRelated.subtitleConfig.en = isChecked;
90+
} else if (text === ButtonText.SubtitleZh) {
91+
shadowRelated.subtitleConfig.zh = isChecked;
8092
}
93+
shadowRelated.onUpdateTranslationConfig();
94+
}, []);
8195

82-
shadowRelated.shouldResotrePunct = shouldResotrePunct;
96+
const [fontSize, setFontSize] = useState(24);
97+
const onSetFontSize = useCallback<IScrollBarComponentProps["onScrolling"]>((sender) => {
98+
const fontSize = sender.GetValue();
99+
shadowRelated.onUpdateFontSize(fontSize);
100+
setFontSize(fontSize);
83101
}, []);
84102

85103
useEffect(() => {
@@ -94,16 +112,28 @@ export function Echo() {
94112
<Grid style={{ layout: containerLayout }}>
95113
<Grid style={{ area: containerLayout.areas.control, layout: controlLayout }}>
96114
<Grid style={{ area: controlLayout.areas.measure }}>
97-
<Button text="设置字幕区" iconInfo={{ name: "measure", size: 16 }} onClick={onMeasure}></Button>
115+
<Button text={ButtonText.Measure} iconInfo={{ name: "measure", size: 16 }} onClick={onMeasure}></Button>
98116
</Grid>
99117
<Grid style={{ area: controlLayout.areas.recognize }}>
100-
<CheckBox text="语音识别" value={CheckValue.Unchecked} onCheck={onSetRecognize}></CheckBox>
101-
</Grid>
102-
<Grid style={{ area: controlLayout.areas.punct }}>
103-
<CheckBox text="标点恢复" value={CheckValue.Unchecked} onCheck={onSetPunct}></CheckBox>
118+
<CheckBox text={ButtonText.Recognize} value={CheckValue.Unchecked} onCheck={onSetRecognize}></CheckBox>
104119
</Grid>
105120
<Grid style={{ area: controlLayout.areas.topmost }}>
106-
<CheckBox text="字幕置顶" value={CheckValue.Checked} onCheck={onSetTopMost}></CheckBox>
121+
<CheckBox text={ButtonText.SetTopMost} value={CheckValue.Checked} onCheck={onSetTopMost}></CheckBox>
122+
</Grid>
123+
<Grid style={{ area: controlLayout.areas.en }}>
124+
<CheckBox text={ButtonText.SubtitleEn} value={CheckValue.Checked} onCheck={onSetDisplaySubtitle}></CheckBox>
125+
</Grid>
126+
<Grid style={{ area: controlLayout.areas.zh }}>
127+
<CheckBox text={ButtonText.SubtitleZh} value={CheckValue.Checked} onCheck={onSetDisplaySubtitle}></CheckBox>
128+
</Grid>
129+
<Grid style={{ area: controlLayout.areas.fontSizeLabel }}>
130+
<Label text="字体大小"></Label>
131+
</Grid>
132+
<Grid style={{ area: controlLayout.areas.fontSize, margin: "10dpx 0 10dpx 0" }}>
133+
<ScrollBar min={10} max={50} value={24} /** default value */ onScrolling={onSetFontSize}></ScrollBar>
134+
</Grid>
135+
<Grid style={{ area: controlLayout.areas.fontSizeValue }}>
136+
<Label text={`${fontSize}`}></Label>
107137
</Grid>
108138
</Grid>
109139
</Grid>

src/asr/asr.ts

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ import axios from "axios";
22
import path from "path";
33
import fs from "fs";
44
import childProcess from "child_process";
5-
import { IAsrEngine, IAsrEngineOptions, IAsrResult } from "./base";
6-
import { shadowRelated } from "../shadow";
5+
import { IAsrEngine, IAsrEngineOptions, ISentence } from "./base";
6+
import { TranslationSession } from "./postasr";
7+
import { emptySentence } from "../shadow";
78

89
export class VoskAsrEngine implements IAsrEngine {
910
private options: IAsrEngineOptions;
1011
private asr: childProcess.ChildProcessWithoutNullStreams;
12+
private sessionList: Array<TranslationSession>;
1113

1214
constructor(options: IAsrEngineOptions) {
1315
this.options = options;
16+
this.sessionList = [];
1417
}
1518

1619
async init() {
@@ -53,29 +56,55 @@ export class VoskAsrEngine implements IAsrEngine {
5356
}
5457
}
5558

56-
async recognize(): Promise<IAsrResult> {
57-
// const base64 = buffer.toString("base64");
58-
let text = "";
59+
getCurrentSession() {
60+
return this.sessionList[this.sessionList.length - 1] || null;
61+
}
62+
63+
async addAsrTextToSession(asrText: string, time: number) {
64+
if (!asrText) {
65+
const session = this.getCurrentSession();
66+
if (session) {
67+
// handle only one speech case
68+
await session.flushSpeech();
69+
70+
// create new session if the last is not empty
71+
if (!session.isEmpty()) {
72+
const newSession = new TranslationSession();
73+
this.sessionList.push(newSession);
74+
}
75+
} else {
76+
// it's the first session
77+
this.sessionList.push(new TranslationSession());
78+
}
79+
return;
80+
}
81+
82+
const session = this.getCurrentSession();
83+
if (session) {
84+
await session.addAsrText(asrText, time);
85+
}
86+
}
87+
88+
async recognize(): Promise<ISentence> {
89+
let sentence: ISentence = emptySentence;
5990
try {
6091
const timeout = this.options?.timeout || 3000;
6192
const response = await axios.post("http://localhost:8200/asr", {}, { timeout });
62-
const data = JSON.parse(response.data.result);
63-
console.log(data);
93+
console.log(response.data);
6494

65-
text = data.partial || data.text || "";
95+
const data = JSON.parse(response?.data?.result || "{}");
6696

67-
if (text && shadowRelated.shouldResotrePunct) {
68-
const withPunctResponse = await axios.post("http://localhost:8200/punct", { text }, { timeout });
69-
if (withPunctResponse.data.text) {
70-
text = withPunctResponse.data.text;
71-
console.log({ text });
72-
}
73-
}
97+
const asrText = data.partial || "";
98+
await this.addAsrTextToSession(asrText, Date.now());
99+
100+
const session = this.getCurrentSession();
101+
sentence = session.getCurrentSentence();
102+
sentence.asr = asrText;
74103
} catch (error) {
75104
console.log(`asr failed: ${error.message}`);
76105
this.options?.onError(error.message);
77106
} finally {
78-
return { text };
107+
return sentence;
79108
}
80109
}
81110
}

src/asr/base.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1-
export interface IAsrResult {
1+
export interface ISentence {
22
text: string;
3+
time: number;
4+
sessionId: number;
5+
speechId: number;
6+
asr?: string
7+
}
8+
9+
export interface IAsrText {
10+
text: string;
11+
time: number;
312
}
413

514
export interface IAsrEngineOptions {
@@ -13,7 +22,7 @@ export interface IAsrEngineConstructor {
1322
}
1423

1524
export interface IAsrEngine {
16-
recognize(): Promise<IAsrResult>;
25+
recognize(): Promise<ISentence>;
1726
init(): void;
1827
destroy(): void;
1928
}

src/asr/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export * from "./asr";
1+
export * from "./asr";
2+
export * from "./base";

0 commit comments

Comments
 (0)