Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in / Register
Toggle navigation
C
CharIP-Electron
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ali
CharIP-Electron
Commits
fd1558a6
You need to sign in or sign up before continuing.
Commit
fd1558a6
authored
Nov 28, 2023
by
ali
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: TTS 接口接入,完成各项功能支持配置功能
parent
90ce5548
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
1198 additions
and
1002 deletions
+1198
-1002
settings.json
.vscode/settings.json
+1
-1
README.md
README.md
+1
-1
config.js
buildAssets/builder/config.js
+1
-1
IPCs.ts
src/main/IPCs.ts
+44
-29
DefaultLayout.vue
src/renderer/components/layout/DefaultLayout.vue
+4
-4
HeaderLayout.vue
src/renderer/components/layout/HeaderLayout.vue
+28
-39
main.ts
src/renderer/main.ts
+2
-2
index.ts
src/renderer/plugins/asr/index.ts
+1
-1
interfaces.d.ts
src/renderer/plugins/asr/vosk/interfaces.d.ts
+76
-60
model.d.ts
src/renderer/plugins/asr/vosk/model.d.ts
+50
-39
logging.d.ts
src/renderer/plugins/asr/vosk/utils/logging.d.ts
+9
-9
vosk.d.ts
src/renderer/plugins/asr/vosk/vosk.d.ts
+1
-1
vosk.js
src/renderer/plugins/asr/vosk/vosk.js
+600
-508
worker.d.ts
src/renderer/plugins/asr/vosk/worker.d.ts
+23
-23
FetchTTS.ts
src/renderer/plugins/tts/FetchTTS.ts
+34
-22
index.ts
src/renderer/plugins/tts/index.ts
+1
-1
recognizer-processor.js
src/renderer/public/vosk/recognizer-processor.js
+33
-33
PhotoScreen.vue
src/renderer/screens/PhotoScreen.vue
+55
-35
ShowPhoto.vue
src/renderer/screens/ShowPhoto.vue
+172
-131
index.ts
src/renderer/store/index.ts
+3
-3
photo.ts
src/renderer/store/photo.ts
+14
-15
settings.ts
src/renderer/store/settings.ts
+44
-37
tsconfig.json
tsconfig.json
+1
-7
No files found.
.vscode/settings.json
View file @
fd1558a6
...
@@ -21,5 +21,5 @@
...
@@ -21,5 +21,5 @@
"cSpell.words"
:
[
"cSpell.words"
:
[
"Vosk"
"Vosk"
],
],
"editor.inlineSuggest.showToolbar"
:
"
always
"
"editor.inlineSuggest.showToolbar"
:
"
onHover
"
}
}
README.md
View file @
fd1558a6
# chartIP-Electron
# chartIP-Electron
\ No newline at end of file
buildAssets/builder/config.js
View file @
fd1558a6
...
@@ -69,7 +69,7 @@ const baseConfig = {
...
@@ -69,7 +69,7 @@ const baseConfig = {
oneClick
:
true
oneClick
:
true
},
},
linux
:
{
linux
:
{
executableName
:
'
vutron
'
,
executableName
:
'
chartIP
'
,
icon
:
'buildAssets/icons'
,
icon
:
'buildAssets/icons'
,
category
:
'Utility'
,
category
:
'Utility'
,
target
:
[
target
:
[
...
...
src/main/IPCs.ts
View file @
fd1558a6
...
@@ -5,8 +5,7 @@ import Constants from './utils/Constants'
...
@@ -5,8 +5,7 @@ import Constants from './utils/Constants'
* IPC Communications
* IPC Communications
* */
* */
export
default
class
IPCs
{
export
default
class
IPCs
{
static
browserWindows
:
Map
<
string
,
BrowserWindow
[]
>
=
new
Map
()
static
browserWindows
:
Map
<
string
,
BrowserWindow
[]
>
=
new
Map
();
static
initialize
(
window
:
BrowserWindow
):
void
{
static
initialize
(
window
:
BrowserWindow
):
void
{
// Get application version
// Get application version
...
@@ -20,41 +19,57 @@ export default class IPCs {
...
@@ -20,41 +19,57 @@ export default class IPCs {
})
})
// open new window
// open new window
ipcMain
.
on
(
'openWindow'
,
async
(
event
,
url
:
string
,
options
:
BrowserWindowConstructorOptions
&
{
isCloseOther
:
boolean
})
=>
{
ipcMain
.
on
(
const
ops
=
Object
.
assign
({},
{
isCloseOther
:
true
,
frame
:
false
,
useContentSize
:
true
,
webPreferences
:
Constants
.
DEFAULT_WEB_PREFERENCES
},
options
);
'openWindow'
,
async
(
event
,
url
:
string
,
options
:
BrowserWindowConstructorOptions
&
{
isCloseOther
:
boolean
}
)
=>
{
const
ops
=
Object
.
assign
(
{},
{
isCloseOther
:
true
,
frame
:
false
,
useContentSize
:
true
,
webPreferences
:
Constants
.
DEFAULT_WEB_PREFERENCES
},
options
)
if
(
IPCs
.
browserWindows
.
has
(
url
)
&&
ops
.
isCloseOther
)
{
if
(
IPCs
.
browserWindows
.
has
(
url
)
&&
ops
.
isCloseOther
)
{
const
wins
=
IPCs
.
browserWindows
.
get
(
url
);
const
wins
=
IPCs
.
browserWindows
.
get
(
url
)
wins
?.
forEach
(
w
=>
!
w
.
isDestroyed
()
&&
w
.
close
());
wins
?.
forEach
((
w
)
=>
!
w
.
isDestroyed
()
&&
w
.
close
())
IPCs
.
browserWindows
.
set
(
url
,
[]);
IPCs
.
browserWindows
.
set
(
url
,
[])
}
}
const
win
=
new
BrowserWindow
(
ops
)
const
win
=
new
BrowserWindow
(
ops
)
win
.
setMenu
(
null
)
win
.
setMenu
(
null
)
win
.
once
(
'ready-to-show'
,
():
void
=>
{
win
.
once
(
'ready-to-show'
,
():
void
=>
{
win
.
setAlwaysOnTop
(
true
)
win
.
setAlwaysOnTop
(
true
)
win
.
show
()
win
.
show
()
win
.
focus
()
win
.
focus
()
win
.
setAlwaysOnTop
(
false
)
win
.
setAlwaysOnTop
(
false
)
})
})
win
.
webContents
.
on
(
'did-frame-finish-load'
,
():
void
=>
{
win
.
webContents
.
on
(
'did-frame-finish-load'
,
():
void
=>
{
if
(
Constants
.
IS_DEV_ENV
)
{
if
(
Constants
.
IS_DEV_ENV
)
{
win
.
webContents
.
openDevTools
()
win
.
webContents
.
openDevTools
()
}
}
})
})
await
win
.
loadURL
(
`
${
Constants
.
APP_INDEX_URL_DEV
}${
url
}
`
)
await
win
.
loadURL
(
`
${
Constants
.
APP_INDEX_URL_DEV
}${
url
}
`
)
if
(
!
IPCs
.
browserWindows
.
has
(
url
))
{
if
(
!
IPCs
.
browserWindows
.
has
(
url
))
{
IPCs
.
browserWindows
.
set
(
url
,
[]);
IPCs
.
browserWindows
.
set
(
url
,
[])
}
}
IPCs
.
browserWindows
.
get
(
url
)?.
push
(
win
);
IPCs
.
browserWindows
.
get
(
url
)?.
push
(
win
)
return
win
;
return
win
})
}
)
}
}
}
}
src/renderer/components/layout/DefaultLayout.vue
View file @
fd1558a6
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
HeaderLayout
from
'@/renderer/components/layout/HeaderLayout.vue'
import
HeaderLayout
from
'@/renderer/components/layout/HeaderLayout.vue'
import
{
ref
}
from
'vue'
;
import
{
ref
}
from
'vue'
import
{
useRouter
}
from
'vue-router'
import
{
useRouter
}
from
'vue-router'
const
router
=
useRouter
()
const
router
=
useRouter
()
const
isHeader
=
ref
(
true
)
;
const
isHeader
=
ref
(
true
)
router
.
beforeEach
((
guard
)
=>
{
router
.
beforeEach
((
guard
)
=>
{
isHeader
.
value
=
typeof
guard
.
meta
.
isHeader
===
'boolean'
?
(
guard
.
meta
.
isHeader
as
boolean
)
:
true
;
isHeader
.
value
=
typeof
guard
.
meta
.
isHeader
===
'boolean'
?
(
guard
.
meta
.
isHeader
as
boolean
)
:
true
})
})
</
script
>
</
script
>
<
template
>
<
template
>
...
...
src/renderer/components/layout/HeaderLayout.vue
View file @
fd1558a6
<
script
setup
lang=
"tsx"
>
<
script
setup
lang=
"tsx"
>
import
{
computed
,
ref
}
from
'vue'
;
import
{
computed
,
ref
}
from
'vue'
import
{
useRoute
,
useRouter
}
from
'vue-router'
import
{
useRoute
,
useRouter
}
from
'vue-router'
import
useStore
from
'@/renderer/store'
;
import
useStore
from
'@/renderer/store'
import
{
storeToRefs
}
from
'pinia'
;
import
{
storeToRefs
}
from
'pinia'
import
{
audioAiTTS
}
from
'@/renderer/plugins/tts'
import
{
audioAiTTS
}
from
'@/renderer/plugins/tts'
const
router
=
useRouter
()
const
router
=
useRouter
()
const
route
:
any
=
useRoute
()
const
route
:
any
=
useRoute
()
const
{
settings
}
=
useStore
()
;
const
{
settings
}
=
useStore
()
const
setting
=
storeToRefs
(
settings
)
;
const
setting
=
storeToRefs
(
settings
)
settings
.
getSource
()
;
settings
.
getSource
()
const
handleRoute
=
(
path
:
string
):
void
=>
{
const
handleRoute
=
(
path
:
string
):
void
=>
{
router
.
push
(
path
)
router
.
push
(
path
)
...
@@ -25,8 +25,8 @@ const asrItems = ref([
...
@@ -25,8 +25,8 @@ const asrItems = ref([
'vosk_asr'
,
'vosk_asr'
,
'xf_asr'
'xf_asr'
// 'Whisper Api'
// 'Whisper Api'
])
;
])
const
asrSelect
=
ref
(
setting
.
asr
)
;
const
asrSelect
=
ref
(
setting
.
asr
)
const
source
=
computed
(()
=>
{
const
source
=
computed
(()
=>
{
return
setting
.
source
.
value
.
map
(({
sourceId
,
sourceName
,
description
,
sex
})
=>
{
return
setting
.
source
.
value
.
map
(({
sourceId
,
sourceName
,
description
,
sex
})
=>
{
...
@@ -35,17 +35,22 @@ const source = computed(() => {
...
@@ -35,17 +35,22 @@ const source = computed(() => {
value
:
sourceId
,
value
:
sourceId
,
title
:
`
${
sourceName
}
-
${
_sex
}
-
${
description
}
`
title
:
`
${
sourceName
}
-
${
_sex
}
-
${
description
}
`
}
}
})
;
})
})
;
})
async
function
changeSource
()
{
async
function
changeSource
()
{
const
tone
=
setting
.
source
.
value
.
find
(({
sourceId
})
=>
setting
.
selectSource
.
value
===
sourceId
);
const
tone
=
setting
.
source
.
value
.
find
(({
sourceId
})
=>
setting
.
selectSource
.
value
===
sourceId
)
if
(
!
tone
)
return
;
if
(
!
tone
)
return
const
res
=
await
audioAiTTS
({
host
:
settings
.
ttsHost
,
text
:
'你好,今天天气怎么样?'
,
speed
:
5.5
,
speaker
:
tone
.
sourceId
,
provider
:
tone
.
provider
});
const
res
=
await
audioAiTTS
({
host
:
settings
.
ttsHost
,
console
.
log
(
res
);
text
:
'你好,今天天气怎么样?'
,
speed
:
5.5
,
speaker
:
tone
.
sourceId
,
provider
:
tone
.
provider
})
console
.
log
(
res
)
}
}
</
script
>
</
script
>
<
template
>
<
template
>
<v-app-bar
color=
"#d71b1b"
density=
"compact"
class=
"header"
>
<v-app-bar
color=
"#d71b1b"
density=
"compact"
class=
"header"
>
...
@@ -69,29 +74,20 @@ async function changeSource() {
...
@@ -69,29 +74,20 @@ async function changeSource() {
<v-dialog
width=
"600"
>
<v-dialog
width=
"600"
>
<template
#
activator=
"
{ props }">
<template
#
activator=
"
{ props }">
<v-btn
<v-btn
v-bind=
"props"
color=
"#fff"
class=
"settings"
>
v-bind=
"props"
<v-icon
start
icon=
"mdi-wrench"
></v-icon>
color=
"#fff"
class=
"settings"
>
<v-icon
start
icon=
"mdi-wrench"
></v-icon>
配置
配置
</v-btn>
</v-btn>
</
template
>
</
template
>
<
template
#
default=
"{ isActive }"
>
<
template
#
default=
"{ isActive }"
>
<v-card
title=
"配置"
>
<v-card
title=
"配置"
>
<v-sheet
width=
"500"
class=
"mx-auto mt-6"
>
<v-sheet
width=
"500"
class=
"mx-auto mt-6"
>
<v-form
ref=
"form"
>
<v-form
ref=
"form"
>
<v-select
<v-select
v-model=
"setting.asr.value"
v-model=
"setting.asr.value"
:items=
"asrItems"
:items=
"asrItems"
:rules=
"[
v
=> !!v || '请选择 Asr']"
:rules=
"[
(v)
=> !!v || '请选择 Asr']"
label=
"语音识别(ASR)"
label=
"语音识别(ASR)"
required
required
></v-select>
></v-select>
...
@@ -107,9 +103,7 @@ async function changeSource() {
...
@@ -107,9 +103,7 @@ async function changeSource() {
<v-text-field
<v-text-field
label=
"TTS 域名"
label=
"TTS 域名"
:rules=
"[
:rules=
"[(value) => !!value || 'TTS 域名必填']"
value => !!value || 'TTS 域名必填',
]"
hide-details=
"auto"
hide-details=
"auto"
:model-value=
"setting.ttsHost"
:model-value=
"setting.ttsHost"
></v-text-field>
></v-text-field>
...
@@ -118,26 +112,21 @@ async function changeSource() {
...
@@ -118,26 +112,21 @@ async function changeSource() {
v-model=
"setting.selectSource.value"
v-model=
"setting.selectSource.value"
class=
"mt-6"
class=
"mt-6"
:items=
"source"
:items=
"source"
:rules=
"[
v
=> !!v || '请选择音色']"
:rules=
"[
(v)
=> !!v || '请选择音色']"
label=
"TTS 音色"
label=
"TTS 音色"
required
required
@
update:model-value=
"changeSource"
@
update:model-value=
"changeSource"
></v-select>
></v-select>
</v-form>
</v-form>
</v-sheet>
</v-sheet>
<v-card-actions>
<v-card-actions>
<v-spacer></v-spacer>
<v-spacer></v-spacer>
<v-btn
<v-btn
text=
"关闭"
@
click=
"isActive.value = false"
></v-btn>
text=
"关闭"
@
click=
"isActive.value = false"
></v-btn>
</v-card-actions>
</v-card-actions>
</v-card>
</v-card>
</template>
</template>
</v-dialog>
</v-dialog>
</template>
</template>
</v-app-bar>
</v-app-bar>
</template>
</template>
...
...
src/renderer/main.ts
View file @
fd1558a6
...
@@ -7,8 +7,8 @@ import vuetify from '@/renderer/plugins/vuetify'
...
@@ -7,8 +7,8 @@ import vuetify from '@/renderer/plugins/vuetify'
import
i18n
from
'@/renderer/plugins/i18n'
import
i18n
from
'@/renderer/plugins/i18n'
import
piniaPluginPersistedstate
from
'pinia-plugin-persistedstate'
import
piniaPluginPersistedstate
from
'pinia-plugin-persistedstate'
const
pinia
=
createPinia
()
;
const
pinia
=
createPinia
()
pinia
.
use
(
piniaPluginPersistedstate
)
;
pinia
.
use
(
piniaPluginPersistedstate
)
// Add API key defined in contextBridge to window object type
// Add API key defined in contextBridge to window object type
declare
global
{
declare
global
{
...
...
src/renderer/plugins/asr/index.ts
View file @
fd1558a6
export
*
as
Vosk
from
'./vosk/vosk'
export
*
as
Vosk
from
'./vosk/vosk'
export
type
*
from
'./vosk/vosk'
export
type
*
from
'./vosk/vosk'
\ No newline at end of file
src/renderer/plugins/asr/vosk/interfaces.d.ts
View file @
fd1558a6
export
interface
ClientMessageLoad
{
export
interface
ClientMessageLoad
{
action
:
"load"
;
action
:
'load'
modelUrl
:
string
;
modelUrl
:
string
}
}
export
interface
ClientMessageTerminate
{
export
interface
ClientMessageTerminate
{
action
:
"terminate"
;
action
:
'terminate'
}
}
export
interface
ClientMessageRecognizerSet
{
export
interface
ClientMessageRecognizerSet
{
action
:
"set"
;
action
:
'set'
recognizerId
:
string
;
recognizerId
:
string
key
:
"words"
;
key
:
'words'
value
:
boolean
;
value
:
boolean
}
}
export
interface
ClientMessageGenericSet
{
export
interface
ClientMessageGenericSet
{
action
:
"set"
;
action
:
'set'
key
:
"logLevel"
;
key
:
'logLevel'
value
:
number
;
value
:
number
}
}
export
declare
type
ClientMessageSet
=
ClientMessageRecognizerSet
|
ClientMessageGenericSet
;
export
declare
type
ClientMessageSet
=
ClientMessageRecognizerSet
|
ClientMessageGenericSet
export
interface
ClientMessageAudioChunk
{
export
interface
ClientMessageAudioChunk
{
action
:
"audioChunk"
;
action
:
'audioChunk'
recognizerId
:
string
;
recognizerId
:
string
data
:
Float32Array
;
data
:
Float32Array
sampleRate
:
number
;
sampleRate
:
number
}
}
export
interface
ClientMessageCreateRecognizer
{
export
interface
ClientMessageCreateRecognizer
{
action
:
"create"
;
action
:
'create'
recognizerId
:
string
;
recognizerId
:
string
sampleRate
:
number
;
sampleRate
:
number
grammar
?:
string
;
grammar
?:
string
}
}
export
interface
ClientMessageRetrieveFinalResult
{
export
interface
ClientMessageRetrieveFinalResult
{
action
:
"retrieveFinalResult"
;
action
:
'retrieveFinalResult'
recognizerId
:
string
;
recognizerId
:
string
}
}
export
interface
ClientMessageRemoveRecognizer
{
export
interface
ClientMessageRemoveRecognizer
{
action
:
"remove"
;
action
:
'remove'
recognizerId
:
string
;
recognizerId
:
string
}
}
export
declare
type
ClientMessage
=
ClientMessageTerminate
|
ClientMessageLoad
|
ClientMessageCreateRecognizer
|
ClientMessageAudioChunk
|
ClientMessageSet
|
ClientMessageRetrieveFinalResult
|
ClientMessageRemoveRecognizer
;
export
declare
type
ClientMessage
=
|
ClientMessageTerminate
|
ClientMessageLoad
|
ClientMessageCreateRecognizer
|
ClientMessageAudioChunk
|
ClientMessageSet
|
ClientMessageRetrieveFinalResult
|
ClientMessageRemoveRecognizer
export
declare
namespace
ClientMessage
{
export
declare
namespace
ClientMessage
{
function
isTerminateMessage
(
message
:
ClientMessage
):
message
is
ClientMessageTerminate
;
function
isTerminateMessage
(
message
:
ClientMessage
):
message
is
ClientMessageTerminate
function
isLoadMessage
(
message
:
ClientMessage
):
message
is
ClientMessageLoad
;
function
isLoadMessage
(
message
:
ClientMessage
):
message
is
ClientMessageLoad
function
isSetMessage
(
message
:
ClientMessage
):
message
is
ClientMessageSet
;
function
isSetMessage
(
message
:
ClientMessage
):
message
is
ClientMessageSet
function
isAudioChunkMessage
(
message
:
ClientMessage
):
message
is
ClientMessageAudioChunk
;
function
isAudioChunkMessage
(
message
:
ClientMessage
):
message
is
ClientMessageAudioChunk
function
isRecognizerCreateMessage
(
message
:
ClientMessage
):
message
is
ClientMessageCreateRecognizer
;
function
isRecognizerCreateMessage
(
function
isRecognizerRetrieveFinalResultMessage
(
message
:
ClientMessage
):
message
is
ClientMessageRetrieveFinalResult
;
message
:
ClientMessage
function
isRecognizerRemoveMessage
(
message
:
ClientMessage
):
message
is
ClientMessageRemoveRecognizer
;
):
message
is
ClientMessageCreateRecognizer
function
isRecognizerRetrieveFinalResultMessage
(
message
:
ClientMessage
):
message
is
ClientMessageRetrieveFinalResult
function
isRecognizerRemoveMessage
(
message
:
ClientMessage
):
message
is
ClientMessageRemoveRecognizer
}
}
export
interface
ServerMessageLoadResult
{
export
interface
ServerMessageLoadResult
{
event
:
"load"
;
event
:
'load'
result
:
boolean
;
result
:
boolean
}
}
export
interface
ServerMessageError
{
export
interface
ServerMessageError
{
event
:
"error"
;
event
:
'error'
recognizerId
?:
string
;
recognizerId
?:
string
error
:
string
;
error
:
string
}
}
export
interface
ServerMessageResult
{
export
interface
ServerMessageResult
{
event
:
"result"
;
event
:
'result'
recognizerId
:
string
;
recognizerId
:
string
result
:
{
result
:
{
result
:
Array
<
{
result
:
Array
<
{
conf
:
number
;
conf
:
number
start
:
number
;
start
:
number
end
:
number
;
end
:
number
word
:
string
;
word
:
string
}
>
;
}
>
text
:
string
;
text
:
string
};
}
}
}
export
interface
ServerMessagePartialResult
{
export
interface
ServerMessagePartialResult
{
event
:
"partialresult"
;
event
:
'partialresult'
recognizerId
:
string
;
recognizerId
:
string
result
:
{
result
:
{
partial
:
string
;
partial
:
string
};
}
}
}
export
declare
type
ModelMessage
=
ServerMessageLoadResult
|
ServerMessageError
;
export
declare
type
ModelMessage
=
ServerMessageLoadResult
|
ServerMessageError
export
declare
namespace
ModelMessage
{
export
declare
namespace
ModelMessage
{
function
isLoadResult
(
message
:
any
):
message
is
ServerMessageLoadResult
;
function
isLoadResult
(
message
:
any
):
message
is
ServerMessageLoadResult
}
}
export
declare
type
RecognizerMessage
=
ServerMessagePartialResult
|
ServerMessageResult
|
ServerMessageError
;
export
declare
type
RecognizerMessage
=
export
declare
type
RecognizerEvent
=
RecognizerMessage
[
"event"
];
|
ServerMessagePartialResult
export
declare
type
ServerMessage
=
ModelMessage
|
RecognizerMessage
;
|
ServerMessageResult
|
ServerMessageError
export
declare
type
RecognizerEvent
=
RecognizerMessage
[
'event'
]
export
declare
type
ServerMessage
=
ModelMessage
|
RecognizerMessage
export
declare
namespace
ServerMessage
{
export
declare
namespace
ServerMessage
{
function
isRecognizerMessage
(
message
:
ServerMessage
):
message
is
RecognizerMessage
;
function
isRecognizerMessage
(
message
:
ServerMessage
):
message
is
RecognizerMessage
function
isResult
(
message
:
any
):
message
is
ServerMessageResult
;
function
isResult
(
message
:
any
):
message
is
ServerMessageResult
function
isPartialResult
(
message
:
any
):
message
is
ServerMessagePartialResult
;
function
isPartialResult
(
message
:
any
):
message
is
ServerMessagePartialResult
}
}
src/renderer/plugins/asr/vosk/model.d.ts
View file @
fd1558a6
import
{
ModelMessage
,
RecognizerEvent
,
RecognizerMessage
}
from
"./interfaces"
;
import
{
ModelMessage
,
RecognizerEvent
,
RecognizerMessage
}
from
'./interfaces'
export
*
from
"./interfaces"
;
export
*
from
'./interfaces'
export
declare
class
Model
extends
EventTarget
{
export
declare
class
Model
extends
EventTarget
{
private
modelUrl
;
private
modelUrl
private
worker
;
private
worker
private
_ready
;
private
_ready
private
messagePort
;
private
messagePort
private
logger
;
private
logger
private
recognizers
;
private
recognizers
constructor
(
modelUrl
:
string
,
logLevel
?:
number
);
constructor
(
modelUrl
:
string
,
logLevel
?:
number
)
private
initialize
;
private
initialize
private
postMessage
;
private
postMessage
private
handleMessage
;
private
handleMessage
on
(
event
:
ModelMessage
[
"event"
],
listener
:
(
message
:
ModelMessage
)
=>
void
):
void
;
on
(
event
:
ModelMessage
[
'event'
],
listener
:
(
message
:
ModelMessage
)
=>
void
):
void
registerPort
(
port
:
MessagePort
):
void
;
registerPort
(
port
:
MessagePort
):
void
private
forwardMessage
;
private
forwardMessage
get
ready
():
boolean
;
get
ready
():
boolean
terminate
():
void
;
terminate
():
void
setLogLevel
(
level
:
number
):
void
;
setLogLevel
(
level
:
number
):
void
registerRecognizer
(
recognizer
:
KaldiRecognizer
):
void
;
registerRecognizer
(
recognizer
:
KaldiRecognizer
):
void
unregisterRecognizer
(
recognizerId
:
string
):
void
;
unregisterRecognizer
(
recognizerId
:
string
):
void
/**
/**
* KaldiRecognizer anonymous class
* KaldiRecognizer anonymous class
*/
*/
get
KaldiRecognizer
():
{
get
KaldiRecognizer
():
{
new
(
sampleRate
:
number
,
grammar
?:
string
):
{
new
(
id
:
string
;
sampleRate
:
number
,
on
(
event
:
RecognizerEvent
,
listener
:
(
message
:
RecognizerMessage
)
=>
void
):
void
;
grammar
?:
string
setWords
(
words
:
boolean
):
void
;
):
{
acceptWaveform
(
buffer
:
AudioBuffer
):
void
;
id
:
string
acceptWaveformFloat
(
buffer
:
Float32Array
,
sampleRate
:
number
):
void
;
on
(
event
:
RecognizerEvent
,
listener
:
(
message
:
RecognizerMessage
)
=>
void
):
void
retrieveFinalResult
():
void
;
setWords
(
words
:
boolean
):
void
remove
():
void
;
acceptWaveform
(
buffer
:
AudioBuffer
):
void
addEventListener
(
type
:
string
,
callback
:
EventListenerOrEventListenerObject
|
null
,
options
?:
boolean
|
AddEventListenerOptions
|
undefined
):
void
;
acceptWaveformFloat
(
buffer
:
Float32Array
,
sampleRate
:
number
):
void
dispatchEvent
(
event
:
Event
):
boolean
;
retrieveFinalResult
():
void
removeEventListener
(
type
:
string
,
callback
:
EventListenerOrEventListenerObject
|
null
,
options
?:
boolean
|
EventListenerOptions
|
undefined
):
void
;
remove
():
void
};
addEventListener
(
};
type
:
string
,
callback
:
EventListenerOrEventListenerObject
|
null
,
options
?:
boolean
|
AddEventListenerOptions
|
undefined
):
void
dispatchEvent
(
event
:
Event
):
boolean
removeEventListener
(
type
:
string
,
callback
:
EventListenerOrEventListenerObject
|
null
,
options
?:
boolean
|
EventListenerOptions
|
undefined
):
void
}
}
}
}
export
declare
type
KaldiRecognizer
=
InstanceType
<
Model
[
"KaldiRecognizer"
]
>
;
export
declare
type
KaldiRecognizer
=
InstanceType
<
Model
[
'KaldiRecognizer'
]
>
export
declare
function
createModel
(
modelUrl
:
string
,
logLevel
?:
number
):
Promise
<
Model
>
;
export
declare
function
createModel
(
modelUrl
:
string
,
logLevel
?:
number
):
Promise
<
Model
>
src/renderer/plugins/asr/vosk/utils/logging.d.ts
View file @
fd1558a6
export
declare
class
Logger
{
export
declare
class
Logger
{
private
logLevel
;
private
logLevel
constructor
(
logLevel
?:
number
);
constructor
(
logLevel
?:
number
)
getLogLevel
():
number
;
getLogLevel
():
number
setLogLevel
(
level
:
number
):
void
;
setLogLevel
(
level
:
number
):
void
error
(
message
:
string
):
void
;
error
(
message
:
string
):
void
warn
(
message
:
string
):
void
;
warn
(
message
:
string
):
void
info
(
message
:
string
):
void
;
info
(
message
:
string
):
void
verbose
(
message
:
string
):
void
;
verbose
(
message
:
string
):
void
debug
(
message
:
string
):
void
;
debug
(
message
:
string
):
void
}
}
src/renderer/plugins/asr/vosk/vosk.d.ts
View file @
fd1558a6
export
*
from
"./model"
;
export
*
from
'./model'
src/renderer/plugins/asr/vosk/vosk.js
View file @
fd1558a6
This diff is collapsed.
Click to expand it.
src/renderer/plugins/asr/vosk/worker.d.ts
View file @
fd1558a6
import
*
as
VoskWasm
from
"./vosk-wasm"
;
import
*
as
VoskWasm
from
'./vosk-wasm'
export
interface
Recognizer
{
export
interface
Recognizer
{
id
:
string
;
id
:
string
buffAddr
?:
number
;
buffAddr
?:
number
buffSize
?:
number
;
buffSize
?:
number
recognizer
:
VoskWasm
.
Recognizer
;
recognizer
:
VoskWasm
.
Recognizer
sampleRate
:
number
;
sampleRate
:
number
words
?:
boolean
;
words
?:
boolean
grammar
?:
string
;
grammar
?:
string
}
}
export
declare
class
RecognizerWorker
{
export
declare
class
RecognizerWorker
{
private
Vosk
;
private
Vosk
private
model
;
private
model
private
recognizers
;
private
recognizers
private
logger
;
private
logger
constructor
();
constructor
()
private
handleMessage
;
private
handleMessage
private
load
;
private
load
private
allocateBuffer
;
private
allocateBuffer
private
freeBuffer
;
private
freeBuffer
private
createRecognizer
;
private
createRecognizer
private
setConfiguration
;
private
setConfiguration
private
processAudioChunk
;
private
processAudioChunk
private
retrieveFinalResult
;
private
retrieveFinalResult
private
removeRecognizer
;
private
removeRecognizer
private
terminate
;
private
terminate
}
}
src/renderer/plugins/tts/FetchTTS.ts
View file @
fd1558a6
export
async
function
audioAiTTS
({
host
,
text
,
speaker
,
speed
,
provider
}:
{
host
:
string
;
text
:
string
;
speaker
:
string
;
speed
:
number
;
provider
:
number
;
})
{
export
async
function
audioAiTTS
({
const
resp
=
await
fetch
(
`
${
host
}
/api/live/audioAI`
,
{
host
,
"headers"
:
{
text
,
"accept"
:
"application/json, text/plain, */*"
,
speaker
,
"content-type"
:
"application/json"
,
speed
,
},
provider
body
:
JSON
.
stringify
({
}:
{
type
:
1
,
host
:
string
text
,
text
:
string
speaker
,
speaker
:
string
speed
,
speed
:
number
provider
,
provider
:
number
pause_points
:
[],
})
{
sceneType
:
1
,
const
resp
=
await
fetch
(
`
${
host
}
/api/live/audioAI`
,
{
gender
:
1
headers
:
{
accept
:
'application/json, text/plain, */*'
,
'content-type'
:
'application/json'
},
body
:
JSON
.
stringify
({
type
:
1
,
text
,
speaker
,
speed
,
provider
,
pause_points
:
[],
sceneType
:
1
,
gender
:
1
}),
}),
method
:
"POST"
,
method
:
'POST'
,
mode
:
"cors"
,
mode
:
'cors'
})
;
})
const
res
=
await
resp
.
json
();
const
res
=
await
resp
.
json
()
if
(
res
.
code
!==
200
)
throw
new
Error
(
JSON
.
stringify
(
res
));
if
(
res
.
code
!==
200
)
throw
new
Error
(
JSON
.
stringify
(
res
))
return
res
.
data
;
return
res
.
data
}
}
\ No newline at end of file
src/renderer/plugins/tts/index.ts
View file @
fd1558a6
export
*
from
'./FetchTTS'
export
*
from
'./FetchTTS'
\ No newline at end of file
src/renderer/public/vosk/recognizer-processor.js
View file @
fd1558a6
class
RecognizerAudioProcessor
extends
AudioWorkletProcessor
{
class
RecognizerAudioProcessor
extends
AudioWorkletProcessor
{
constructor
(
options
)
{
constructor
(
options
)
{
super
(
options
);
super
(
options
)
this
.
port
.
onmessage
=
this
.
_processMessage
.
bind
(
this
);
this
.
port
.
onmessage
=
this
.
_processMessage
.
bind
(
this
)
}
}
_processMessage
(
event
)
{
_processMessage
(
event
)
{
// console.debug(`Received event ${JSON.stringify(event.data, null, 2)}`);
// console.debug(`Received event ${JSON.stringify(event.data, null, 2)}`);
if
(
event
.
data
.
action
===
"init"
)
{
if
(
event
.
data
.
action
===
'init'
)
{
this
.
_recognizerId
=
event
.
data
.
recognizerId
;
this
.
_recognizerId
=
event
.
data
.
recognizerId
this
.
_recognizerPort
=
event
.
ports
[
0
];
this
.
_recognizerPort
=
event
.
ports
[
0
]
}
}
}
}
process
(
inputs
,
outputs
,
parameters
)
{
const
data
=
inputs
[
0
][
0
];
process
(
inputs
,
outputs
,
parameters
)
{
if
(
this
.
_recognizerPort
&&
data
)
{
const
data
=
inputs
[
0
][
0
]
// AudioBuffer samples are represented as floating point numbers between -1.0 and 1.0 whilst
if
(
this
.
_recognizerPort
&&
data
)
{
// Kaldi expects them to be between -32768 and 32767 (the range of a signed int16)
// AudioBuffer samples are represented as floating point numbers between -1.0 and 1.0 whilst
const
audioArray
=
data
.
map
((
value
)
=>
value
*
0x8000
);
// Kaldi expects them to be between -32768 and 32767 (the range of a signed int16)
const
audioArray
=
data
.
map
((
value
)
=>
value
*
0x8000
)
this
.
_recognizerPort
.
postMessage
(
{
this
.
_recognizerPort
.
postMessage
(
action
:
"audioChunk"
,
{
data
:
audioArray
,
action
:
'audioChunk'
,
recognizerId
:
this
.
_recognizerId
,
data
:
audioArray
,
sampleRate
,
// Part of AudioWorkletGlobalScope
recognizerId
:
this
.
_recognizerId
,
},
sampleRate
// Part of AudioWorkletGlobalScope
{
},
transfer
:
[
audioArray
.
buffer
],
{
}
transfer
:
[
audioArray
.
buffer
]
);
}
}
return
true
;
)
}
}
return
true
}
}
}
registerProcessor
(
'recognizer-processor'
,
RecognizerAudioProcessor
)
registerProcessor
(
'recognizer-processor'
,
RecognizerAudioProcessor
)
\ No newline at end of file
src/renderer/screens/PhotoScreen.vue
View file @
fd1558a6
...
@@ -5,11 +5,11 @@
...
@@ -5,11 +5,11 @@
// import { useCounterStore } from '@/renderer/store/counter'
// import { useCounterStore } from '@/renderer/store/counter'
// import { storeToRefs } from 'pinia'
// import { storeToRefs } from 'pinia'
import
{
onMounted
,
ref
}
from
'vue'
import
{
onMounted
,
ref
}
from
'vue'
import
useStore
from
'@/renderer/store'
;
import
useStore
from
'@/renderer/store'
import
{
storeToRefs
}
from
'pinia'
;
import
{
storeToRefs
}
from
'pinia'
const
{
photo
:
usePhoto
}
=
useStore
()
;
const
{
photo
:
usePhoto
}
=
useStore
()
const
photo
=
storeToRefs
(
usePhoto
)
;
const
photo
=
storeToRefs
(
usePhoto
)
// const { availableLocales } = useI18n()
// const { availableLocales } = useI18n()
// const { counterIncrease } = useCounterStore()
// const { counterIncrease } = useCounterStore()
...
@@ -20,70 +20,90 @@ const photo = storeToRefs(usePhoto);
...
@@ -20,70 +20,90 @@ const photo = storeToRefs(usePhoto);
onMounted
(():
void
=>
{
onMounted
(():
void
=>
{
// languages.value = availableLocales
// languages.value = availableLocales
// window.mainApi.receive('msgReceivedVersion', (event: Event, version: string) => {
// window.mainApi.receive('msgReceivedVersion', (event: Event, version: string) => {
// appVersion.value = version
// appVersion.value = version
// })
// })
// window.mainApi.send('msgRequestGetVersion')
// window.mainApi.send('msgRequestGetVersion')
})
})
async
function
handleOpen
(
event
:
Event
,
url
:
string
)
{
async
function
handleOpen
(
event
:
Event
,
url
:
string
)
{
await
window
.
mainApi
.
send
(
'openWindow'
,
`#show?url=
${
url
}
`
,
{
width
:
window
.
screen
.
width
/
4
,
height
:
window
.
screen
.
height
});
await
window
.
mainApi
.
send
(
'openWindow'
,
`#show?url=
${
url
}
`
,
{
width
:
window
.
screen
.
width
/
4
,
height
:
window
.
screen
.
height
})
}
}
const
validateURL
=
(
url
:
string
)
=>
{
const
validateURL
=
(
url
:
string
)
=>
{
const
regex
=
/^
(
https
?
|ftp
)
:
\/\/([\w/\-
?=%.
]
+
\.[\w/\-
?=%.
]
+
)
$/
;
const
regex
=
/^
(
https
?
|ftp
)
:
\/\/([\w/\-
?=%.
]
+
\.[\w/\-
?=%.
]
+
)
$/
return
regex
.
test
(
url
)
;
return
regex
.
test
(
url
)
}
}
const
urlValue
=
ref
(
''
)
;
const
urlValue
=
ref
(
''
)
const
imgLoading
=
ref
(
false
)
;
const
imgLoading
=
ref
(
false
)
async
function
appendPhoto
(
url
:
string
)
{
async
function
appendPhoto
(
url
:
string
)
{
urlValue
.
value
=
url
;
urlValue
.
value
=
url
if
(
!
validateURL
(
url
))
return
'请输入正确的 url!如(url(https://xxx.png)'
if
(
!
validateURL
(
url
))
return
'请输入正确的 url!如(url(https://xxx.png)'
try
{
try
{
imgLoading
.
value
=
true
;
imgLoading
.
value
=
true
const
img
=
new
Image
()
;
const
img
=
new
Image
()
img
.
src
=
url
;
img
.
src
=
url
await
new
Promise
((
resolve
,
reject
)
=>
{
await
new
Promise
((
resolve
,
reject
)
=>
{
img
.
onload
=
resolve
;
img
.
onload
=
resolve
img
.
onerror
=
reject
;
img
.
onerror
=
reject
})
;
})
imgLoading
.
value
=
false
;
imgLoading
.
value
=
false
}
catch
(
error
)
{
}
catch
(
error
)
{
imgLoading
.
value
=
false
;
imgLoading
.
value
=
false
return
'图片加载失败!'
return
'图片加载失败!'
}
}
photo
.
list
.
value
.
push
({
url
})
;
photo
.
list
.
value
.
push
({
url
})
urlValue
.
value
=
''
urlValue
.
value
=
''
return
true
;
return
true
}
}
function
removePhoto
(
index
:
number
)
{
function
removePhoto
(
index
:
number
)
{
photo
.
list
.
value
.
splice
(
index
,
1
)
photo
.
list
.
value
.
splice
(
index
,
1
)
}
}
</
script
>
</
script
>
<
template
>
<
template
>
<v-container
class=
"d-flex mt-6 pb-0"
>
<v-container
class=
"d-flex mt-6 pb-0"
>
<v-text-field
label=
"自定义照片 url(https://xxx.png)"
:model-value=
"urlValue"
:loading=
"imgLoading"
:rules=
"[ v => appendPhoto(v) ]"
validate-on=
"blur lazy"
></v-text-field>
<v-text-field
label=
"自定义照片 url(https://xxx.png)"
:model-value=
"urlValue"
:loading=
"imgLoading"
:rules=
"[(v) => appendPhoto(v)]"
validate-on=
"blur lazy"
></v-text-field>
</v-container>
</v-container>
<v-container
class=
"d-flex flex-wrap"
>
<v-container
class=
"d-flex flex-wrap"
>
<v-sheet
v-for=
"(item, index) in photo.list.value"
:key=
"item.url"
v-ripple
:elevation=
"3"
width=
"200"
class=
"d-flex spacing-playground pa-6 mr-4 mt-4"
rounded
>
<v-sheet
<v-img
v-for=
"(item, index) in photo.list.value"
:width=
"200"
:key=
"item.url"
aspect-ratio=
"1/1"
v-ripple
cover
:elevation=
"3"
:src=
"item.url"
width=
"200"
@
click=
"handleOpen($event, item.url)"
class=
"d-flex spacing-playground pa-6 mr-4 mt-4"
></v-img>
rounded
<v-btn
density=
"compact"
elevation=
"1"
icon=
"mdi-close"
class=
"mt-n7"
@
click=
"removePhoto(index)"
></v-btn>
>
</v-sheet>
<v-img
:width=
"200"
aspect-ratio=
"1/1"
cover
:src=
"item.url"
@
click=
"handleOpen($event, item.url)"
></v-img>
<v-btn
density=
"compact"
elevation=
"1"
icon=
"mdi-close"
class=
"mt-n7"
@
click=
"removePhoto(index)"
></v-btn>
</v-sheet>
</v-container>
</v-container>
</
template
>
</
template
>
src/renderer/screens/ShowPhoto.vue
View file @
fd1558a6
This diff is collapsed.
Click to expand it.
src/renderer/store/index.ts
View file @
fd1558a6
import
useSettings
from
'./settings'
;
import
useSettings
from
'./settings'
import
usePhoto
from
'./photo'
;
import
usePhoto
from
'./photo'
export
default
function
useStore
()
{
export
default
function
useStore
()
{
return
{
return
{
settings
:
useSettings
(),
settings
:
useSettings
(),
photo
:
usePhoto
()
photo
:
usePhoto
()
}
}
}
}
\ No newline at end of file
src/renderer/store/photo.ts
View file @
fd1558a6
...
@@ -6,20 +6,19 @@ type IPhoto = {
...
@@ -6,20 +6,19 @@ type IPhoto = {
const
usePhotoStore
=
defineStore
(
'photo'
,
{
const
usePhotoStore
=
defineStore
(
'photo'
,
{
persist
:
true
,
persist
:
true
,
state
:
()
=>
({
state
:
()
=>
list
:
[
({
{
list
:
[
url
:
'https://resources.laihua.com/2023-11-2/93ffb6a7-ae93-4918-944e-877016ba266b.png'
{
},
url
:
'https://resources.laihua.com/2023-11-2/93ffb6a7-ae93-4918-944e-877016ba266b.png'
{
},
url
:
'https://resources.laihua.com/2023-6-19/6fa9a127-2ce5-43ea-a543-475bf9354eda.png'
{
}
url
:
'https://resources.laihua.com/2023-6-19/6fa9a127-2ce5-43ea-a543-475bf9354eda.png'
]
}
}
as
IPhoto
),
]
getters
:
{
})
as
IPhoto
,
},
getters
:
{},
actions
:
{
actions
:
{}
}
})
})
export
default
usePhotoStore
export
default
usePhotoStore
\ No newline at end of file
src/renderer/store/settings.ts
View file @
fd1558a6
import
{
defineStore
}
from
'pinia'
import
{
defineStore
}
from
'pinia'
export
type
ISettings
=
{
export
type
ISettings
=
{
asr
:
'vosk_asr'
|
'xf_asr'
;
asr
:
'vosk_asr'
|
'xf_asr'
voskModels
:
string
[];
voskModels
:
string
[]
voskSelectModel
:
string
;
voskSelectModel
:
string
ttsHost
:
string
;
ttsHost
:
string
source
:
{
sourceName
:
string
;
sourceId
:
string
;
provider
:
number
;
speaker
:
string
;
description
:
string
;
sex
:
1
|
0
}[];
source
:
{
selectSource
:
string
;
sourceName
:
string
sourceId
:
string
provider
:
number
speaker
:
string
description
:
string
sex
:
1
|
0
}[]
selectSource
:
string
}
}
const
useSettingsStore
=
defineStore
(
'settings'
,
{
const
useSettingsStore
=
defineStore
(
'settings'
,
{
persist
:
true
,
persist
:
true
,
state
:
()
=>
({
state
:
()
=>
asr
:
'vosk_asr'
,
({
voskModels
:
[
asr
:
'vosk_asr'
,
'vosk-model-small-ca-0.4.tar.gz'
,
voskModels
:
[
'vosk-model-small-cn-0.3
.tar.gz'
,
'vosk-model-small-ca-0.4
.tar.gz'
,
'vosk-model-small-de-0.15
.tar.gz'
,
'vosk-model-small-cn-0.3
.tar.gz'
,
'vosk-model-small-en-in-0.4
.tar.gz'
,
'vosk-model-small-de-0.15
.tar.gz'
,
'vosk-model-small-en-us-0.15
.tar.gz'
,
'vosk-model-small-en-in-0.4
.tar.gz'
,
'vosk-model-small-es-0.3
.tar.gz'
,
'vosk-model-small-en-us-0.15
.tar.gz'
,
'vosk-model-small-fa-0.4
.tar.gz'
,
'vosk-model-small-es-0.3
.tar.gz'
,
'vosk-model-small-fr-pguyot-0.3
.tar.gz'
,
'vosk-model-small-fa-0.4
.tar.gz'
,
'vosk-model-small-it-0.4
.tar.gz'
,
'vosk-model-small-fr-pguyot-0.3
.tar.gz'
,
'vosk-model-small-pt-0.3
.tar.gz'
,
'vosk-model-small-it-0.4
.tar.gz'
,
'vosk-model-small-ru-0.4
.tar.gz'
,
'vosk-model-small-pt-0.3
.tar.gz'
,
'vosk-model-small-tr-0.3
.tar.gz'
,
'vosk-model-small-ru-0.4
.tar.gz'
,
'vosk-model-small-vn-0.3.tar.gz'
'vosk-model-small-tr-0.3.tar.gz'
,
],
'vosk-model-small-vn-0.3.tar.gz'
voskSelectModel
:
'vosk-model-small-cn-0.3.tar.gz'
,
]
,
ttsHost
:
'https://beta.laihua.com
'
,
voskSelectModel
:
'vosk-model-small-cn-0.3.tar.gz
'
,
source
:
[]
,
ttsHost
:
'https://beta.laihua.com'
,
selectSource
:
''
source
:
[],
}
as
ISettings
),
selectSource
:
''
getters
:
{
})
as
ISettings
,
},
getters
:
{
},
actions
:
{
actions
:
{
async
getSource
()
{
async
getSource
()
{
const
resp
=
await
fetch
(
`
${
this
.
$state
.
ttsHost
}
/api/live/audioAI/source?platform=31`
,
{
const
resp
=
await
fetch
(
`
${
this
.
$state
.
ttsHost
}
/api/live/audioAI/source?platform=31`
,
{
"method"
:
"GET"
,
method
:
'GET'
,
"mode"
:
"cors"
,
mode
:
'cors'
})
;
})
const
res
=
await
resp
.
json
()
;
const
res
=
await
resp
.
json
()
if
(
res
.
code
!==
200
)
return
;
if
(
res
.
code
!==
200
)
return
this
.
source
=
res
.
data
;
this
.
source
=
res
.
data
}
}
}
}
})
})
export
default
useSettingsStore
;
export
default
useSettingsStore
tsconfig.json
View file @
fd1558a6
...
@@ -26,11 +26,5 @@
...
@@ -26,11 +26,5 @@
"path"
:
"./tsconfig.node.json"
"path"
:
"./tsconfig.node.json"
}
}
],
],
"exclude"
:
[
"exclude"
:
[
"node_modules"
,
"dist"
,
"rollup.config.js"
,
"*.json"
,
"*.js"
]
"node_modules"
,
"dist"
,
"rollup.config.js"
,
"*.json"
,
"*.js"
]
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment