From f8832cb5f219887e63e095c671549c264a226899 Mon Sep 17 00:00:00 2001 From: yujinqiu Date: Mon, 1 Apr 2024 20:34:14 +0800 Subject: [PATCH 01/45] Add language identification swiftui demo (#729) --- ios-swiftui/SherpaOnnx/SherpaOnnx/Model.swift | 22 + .../project.pbxproj | 667 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon 1.appiconset/Contents.json | 14 + .../AppIcon 1.appiconset/k2-1024x1024.png | Bin 0 -> 421090 bytes .../AppIcon.appiconset/Contents.json | 13 + .../Assets.xcassets/Contents.json | 6 + .../SherpaOnnxLangID/ContentView.swift | 46 ++ .../Preview Assets.xcassets/Contents.json | 6 + .../SherpaOnnxLangIDApp.swift | 17 + .../SherpaOnnxLangID/ViewModel.swift | 131 ++++ .../SherpaOnnxLangIDTests.swift | 36 + .../SherpaOnnxLangIDUITests.swift | 41 ++ .../SherpaOnnxLangIDUITestsLaunchTests.swift | 32 + 16 files changed, 1057 insertions(+) create mode 100644 ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID.xcodeproj/project.pbxproj create mode 100644 ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/Assets.xcassets/AppIcon 1.appiconset/Contents.json create mode 100644 ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/Assets.xcassets/AppIcon 1.appiconset/k2-1024x1024.png create mode 100644 ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/Assets.xcassets/Contents.json create mode 100644 ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/ContentView.swift create mode 100644 ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/SherpaOnnxLangIDApp.swift create mode 100644 ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/ViewModel.swift create mode 100644 ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangIDTests/SherpaOnnxLangIDTests.swift create mode 100644 ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangIDUITests/SherpaOnnxLangIDUITests.swift create mode 100644 ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangIDUITests/SherpaOnnxLangIDUITestsLaunchTests.swift diff --git a/ios-swiftui/SherpaOnnx/SherpaOnnx/Model.swift b/ios-swiftui/SherpaOnnx/SherpaOnnx/Model.swift index a8439f8e0..d53ff6d5b 100644 --- a/ios-swiftui/SherpaOnnx/SherpaOnnx/Model.swift +++ b/ios-swiftui/SherpaOnnx/SherpaOnnx/Model.swift @@ -48,6 +48,28 @@ func getBilingualStreamingZhEnParaformer() -> SherpaOnnxOnlineModelConfig { ) } +// https://k2-fsa.github.io/sherpa/onnx/pretrained_models/whisper/tiny.en.html#tiny-en +// +func getLanguageIdentificationTiny() -> SherpaOnnxSpokenLanguageIdentificationConfig + { + let encoder = getResource("tiny-encoder.int8", "onnx") + let decoder = getResource("tiny-decoder.int8", "onnx") + + let whisperConfig = sherpaOnnxSpokenLanguageIdentificationWhisperConfig( + encoder: encoder, + decoder: decoder + ) + + let config = sherpaOnnxSpokenLanguageIdentificationConfig( + whisper: whisperConfig, + numThreads: 1, + debug: 1, + provider: "cpu" + ) + return config +} + + /// Please refer to /// https://k2-fsa.github.io/sherpa/onnx/pretrained_models/index.html /// to add more models if you need diff --git a/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID.xcodeproj/project.pbxproj b/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID.xcodeproj/project.pbxproj new file mode 100644 index 000000000..462741c81 --- /dev/null +++ b/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID.xcodeproj/project.pbxproj @@ -0,0 +1,667 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + DEBB2D762BBAAA3500864EF5 /* SherpaOnnxLangIDApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBB2D752BBAAA3500864EF5 /* SherpaOnnxLangIDApp.swift */; }; + DEBB2D782BBAAA3500864EF5 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBB2D772BBAAA3500864EF5 /* ContentView.swift */; }; + DEBB2D7A2BBAAA3600864EF5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DEBB2D792BBAAA3600864EF5 /* Assets.xcassets */; }; + DEBB2D7D2BBAAA3600864EF5 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DEBB2D7C2BBAAA3600864EF5 /* Preview Assets.xcassets */; }; + DEBB2D872BBAAA3600864EF5 /* SherpaOnnxLangIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBB2D862BBAAA3600864EF5 /* SherpaOnnxLangIDTests.swift */; }; + DEBB2D912BBAAA3600864EF5 /* SherpaOnnxLangIDUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBB2D902BBAAA3600864EF5 /* SherpaOnnxLangIDUITests.swift */; }; + DEBB2D932BBAAA3600864EF5 /* SherpaOnnxLangIDUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBB2D922BBAAA3600864EF5 /* SherpaOnnxLangIDUITestsLaunchTests.swift */; }; + DEBB2DA12BBAAAD800864EF5 /* Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBB2DA02BBAAAD800864EF5 /* Extension.swift */; }; + DEBB2DA32BBAAAE700864EF5 /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBB2DA22BBAAAE700864EF5 /* Model.swift */; }; + DEBB2DA52BBAAAFD00864EF5 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBB2DA42BBAAAFD00864EF5 /* ViewModel.swift */; }; + DEBB2DAC2BBAAC6200864EF5 /* onnxruntime.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DEBB2DAB2BBAAC6200864EF5 /* onnxruntime.xcframework */; }; + DEBB2DAD2BBAAC6200864EF5 /* onnxruntime.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DEBB2DAB2BBAAC6200864EF5 /* onnxruntime.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + DEBB2DAF2BBAAC6400864EF5 /* sherpa-onnx.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DEBB2DA72BBAAC4D00864EF5 /* sherpa-onnx.xcframework */; }; + DEBB2DB02BBAAC6400864EF5 /* sherpa-onnx.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DEBB2DA72BBAAC4D00864EF5 /* sherpa-onnx.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + DEBB2DB22BBAAD0000864EF5 /* SherpaOnnx.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBB2DB12BBAAD0000864EF5 /* SherpaOnnx.swift */; }; + DEBB2DB42BBAB02E00864EF5 /* tiny-decoder.int8.onnx in Resources */ = {isa = PBXBuildFile; fileRef = DEBB2DB32BBAB02E00864EF5 /* tiny-decoder.int8.onnx */; }; + DEBB2DB62BBAB03400864EF5 /* tiny-encoder.int8.onnx in Resources */ = {isa = PBXBuildFile; fileRef = DEBB2DB52BBAB03400864EF5 /* tiny-encoder.int8.onnx */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + DEBB2D832BBAAA3600864EF5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DEBB2D6A2BBAAA3500864EF5 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DEBB2D712BBAAA3500864EF5; + remoteInfo = SherpaOnnxLangID; + }; + DEBB2D8D2BBAAA3600864EF5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DEBB2D6A2BBAAA3500864EF5 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DEBB2D712BBAAA3500864EF5; + remoteInfo = SherpaOnnxLangID; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + DEBB2DAE2BBAAC6200864EF5 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + DEBB2DAD2BBAAC6200864EF5 /* onnxruntime.xcframework in Embed Frameworks */, + DEBB2DB02BBAAC6400864EF5 /* sherpa-onnx.xcframework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + DEBB2D722BBAAA3500864EF5 /* SherpaOnnxLangID.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SherpaOnnxLangID.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DEBB2D752BBAAA3500864EF5 /* SherpaOnnxLangIDApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SherpaOnnxLangIDApp.swift; sourceTree = ""; }; + DEBB2D772BBAAA3500864EF5 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + DEBB2D792BBAAA3600864EF5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + DEBB2D7C2BBAAA3600864EF5 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + DEBB2D822BBAAA3600864EF5 /* SherpaOnnxLangIDTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SherpaOnnxLangIDTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DEBB2D862BBAAA3600864EF5 /* SherpaOnnxLangIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SherpaOnnxLangIDTests.swift; sourceTree = ""; }; + DEBB2D8C2BBAAA3600864EF5 /* SherpaOnnxLangIDUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SherpaOnnxLangIDUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DEBB2D902BBAAA3600864EF5 /* SherpaOnnxLangIDUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SherpaOnnxLangIDUITests.swift; sourceTree = ""; }; + DEBB2D922BBAAA3600864EF5 /* SherpaOnnxLangIDUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SherpaOnnxLangIDUITestsLaunchTests.swift; sourceTree = ""; }; + DEBB2D9F2BBAAACD00864EF5 /* SherpaOnnx-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SherpaOnnx-Bridging-Header.h"; path = "../../../swift-api-examples/SherpaOnnx-Bridging-Header.h"; sourceTree = ""; }; + DEBB2DA02BBAAAD800864EF5 /* Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Extension.swift; path = ../../SherpaOnnx/SherpaOnnx/Extension.swift; sourceTree = ""; }; + DEBB2DA22BBAAAE700864EF5 /* Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Model.swift; path = ../../SherpaOnnx/SherpaOnnx/Model.swift; sourceTree = ""; }; + DEBB2DA42BBAAAFD00864EF5 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; }; + DEBB2DA72BBAAC4D00864EF5 /* sherpa-onnx.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = "sherpa-onnx.xcframework"; path = "../../build-ios/sherpa-onnx.xcframework"; sourceTree = ""; }; + DEBB2DAB2BBAAC6200864EF5 /* onnxruntime.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = onnxruntime.xcframework; path = "../../build-ios/ios-onnxruntime/1.17.1/onnxruntime.xcframework"; sourceTree = ""; }; + DEBB2DB12BBAAD0000864EF5 /* SherpaOnnx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SherpaOnnx.swift; path = "../../../swift-api-examples/SherpaOnnx.swift"; sourceTree = ""; }; + DEBB2DB32BBAB02E00864EF5 /* tiny-decoder.int8.onnx */ = {isa = PBXFileReference; lastKnownFileType = file; path = "tiny-decoder.int8.onnx"; sourceTree = ""; }; + DEBB2DB52BBAB03400864EF5 /* tiny-encoder.int8.onnx */ = {isa = PBXFileReference; lastKnownFileType = file; path = "tiny-encoder.int8.onnx"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + DEBB2D6F2BBAAA3500864EF5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DEBB2DAC2BBAAC6200864EF5 /* onnxruntime.xcframework in Frameworks */, + DEBB2DAF2BBAAC6400864EF5 /* sherpa-onnx.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEBB2D7F2BBAAA3600864EF5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEBB2D892BBAAA3600864EF5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + DEBB2D692BBAAA3500864EF5 = { + isa = PBXGroup; + children = ( + DEBB2D742BBAAA3500864EF5 /* SherpaOnnxLangID */, + DEBB2D852BBAAA3600864EF5 /* SherpaOnnxLangIDTests */, + DEBB2D8F2BBAAA3600864EF5 /* SherpaOnnxLangIDUITests */, + DEBB2D732BBAAA3500864EF5 /* Products */, + DEBB2DA62BBAAC4D00864EF5 /* Frameworks */, + ); + sourceTree = ""; + }; + DEBB2D732BBAAA3500864EF5 /* Products */ = { + isa = PBXGroup; + children = ( + DEBB2D722BBAAA3500864EF5 /* SherpaOnnxLangID.app */, + DEBB2D822BBAAA3600864EF5 /* SherpaOnnxLangIDTests.xctest */, + DEBB2D8C2BBAAA3600864EF5 /* SherpaOnnxLangIDUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + DEBB2D742BBAAA3500864EF5 /* SherpaOnnxLangID */ = { + isa = PBXGroup; + children = ( + DEBB2DB32BBAB02E00864EF5 /* tiny-decoder.int8.onnx */, + DEBB2D752BBAAA3500864EF5 /* SherpaOnnxLangIDApp.swift */, + DEBB2DB52BBAB03400864EF5 /* tiny-encoder.int8.onnx */, + DEBB2D772BBAAA3500864EF5 /* ContentView.swift */, + DEBB2DA42BBAAAFD00864EF5 /* ViewModel.swift */, + DEBB2D9F2BBAAACD00864EF5 /* SherpaOnnx-Bridging-Header.h */, + DEBB2DB12BBAAD0000864EF5 /* SherpaOnnx.swift */, + DEBB2DA02BBAAAD800864EF5 /* Extension.swift */, + DEBB2DA22BBAAAE700864EF5 /* Model.swift */, + DEBB2D792BBAAA3600864EF5 /* Assets.xcassets */, + DEBB2D7B2BBAAA3600864EF5 /* Preview Content */, + ); + path = SherpaOnnxLangID; + sourceTree = ""; + }; + DEBB2D7B2BBAAA3600864EF5 /* Preview Content */ = { + isa = PBXGroup; + children = ( + DEBB2D7C2BBAAA3600864EF5 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + DEBB2D852BBAAA3600864EF5 /* SherpaOnnxLangIDTests */ = { + isa = PBXGroup; + children = ( + DEBB2D862BBAAA3600864EF5 /* SherpaOnnxLangIDTests.swift */, + ); + path = SherpaOnnxLangIDTests; + sourceTree = ""; + }; + DEBB2D8F2BBAAA3600864EF5 /* SherpaOnnxLangIDUITests */ = { + isa = PBXGroup; + children = ( + DEBB2D902BBAAA3600864EF5 /* SherpaOnnxLangIDUITests.swift */, + DEBB2D922BBAAA3600864EF5 /* SherpaOnnxLangIDUITestsLaunchTests.swift */, + ); + path = SherpaOnnxLangIDUITests; + sourceTree = ""; + }; + DEBB2DA62BBAAC4D00864EF5 /* Frameworks */ = { + isa = PBXGroup; + children = ( + DEBB2DAB2BBAAC6200864EF5 /* onnxruntime.xcframework */, + DEBB2DA72BBAAC4D00864EF5 /* sherpa-onnx.xcframework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + DEBB2D712BBAAA3500864EF5 /* SherpaOnnxLangID */ = { + isa = PBXNativeTarget; + buildConfigurationList = DEBB2D962BBAAA3600864EF5 /* Build configuration list for PBXNativeTarget "SherpaOnnxLangID" */; + buildPhases = ( + DEBB2D6E2BBAAA3500864EF5 /* Sources */, + DEBB2D6F2BBAAA3500864EF5 /* Frameworks */, + DEBB2D702BBAAA3500864EF5 /* Resources */, + DEBB2DAE2BBAAC6200864EF5 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SherpaOnnxLangID; + productName = SherpaOnnxLangID; + productReference = DEBB2D722BBAAA3500864EF5 /* SherpaOnnxLangID.app */; + productType = "com.apple.product-type.application"; + }; + DEBB2D812BBAAA3600864EF5 /* SherpaOnnxLangIDTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = DEBB2D992BBAAA3600864EF5 /* Build configuration list for PBXNativeTarget "SherpaOnnxLangIDTests" */; + buildPhases = ( + DEBB2D7E2BBAAA3600864EF5 /* Sources */, + DEBB2D7F2BBAAA3600864EF5 /* Frameworks */, + DEBB2D802BBAAA3600864EF5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DEBB2D842BBAAA3600864EF5 /* PBXTargetDependency */, + ); + name = SherpaOnnxLangIDTests; + productName = SherpaOnnxLangIDTests; + productReference = DEBB2D822BBAAA3600864EF5 /* SherpaOnnxLangIDTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + DEBB2D8B2BBAAA3600864EF5 /* SherpaOnnxLangIDUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = DEBB2D9C2BBAAA3600864EF5 /* Build configuration list for PBXNativeTarget "SherpaOnnxLangIDUITests" */; + buildPhases = ( + DEBB2D882BBAAA3600864EF5 /* Sources */, + DEBB2D892BBAAA3600864EF5 /* Frameworks */, + DEBB2D8A2BBAAA3600864EF5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DEBB2D8E2BBAAA3600864EF5 /* PBXTargetDependency */, + ); + name = SherpaOnnxLangIDUITests; + productName = SherpaOnnxLangIDUITests; + productReference = DEBB2D8C2BBAAA3600864EF5 /* SherpaOnnxLangIDUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + DEBB2D6A2BBAAA3500864EF5 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1530; + LastUpgradeCheck = 1530; + TargetAttributes = { + DEBB2D712BBAAA3500864EF5 = { + CreatedOnToolsVersion = 15.3; + }; + DEBB2D812BBAAA3600864EF5 = { + CreatedOnToolsVersion = 15.3; + TestTargetID = DEBB2D712BBAAA3500864EF5; + }; + DEBB2D8B2BBAAA3600864EF5 = { + CreatedOnToolsVersion = 15.3; + TestTargetID = DEBB2D712BBAAA3500864EF5; + }; + }; + }; + buildConfigurationList = DEBB2D6D2BBAAA3500864EF5 /* Build configuration list for PBXProject "SherpaOnnxLangID" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = DEBB2D692BBAAA3500864EF5; + productRefGroup = DEBB2D732BBAAA3500864EF5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + DEBB2D712BBAAA3500864EF5 /* SherpaOnnxLangID */, + DEBB2D812BBAAA3600864EF5 /* SherpaOnnxLangIDTests */, + DEBB2D8B2BBAAA3600864EF5 /* SherpaOnnxLangIDUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + DEBB2D702BBAAA3500864EF5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DEBB2D7D2BBAAA3600864EF5 /* Preview Assets.xcassets in Resources */, + DEBB2DB62BBAB03400864EF5 /* tiny-encoder.int8.onnx in Resources */, + DEBB2DB42BBAB02E00864EF5 /* tiny-decoder.int8.onnx in Resources */, + DEBB2D7A2BBAAA3600864EF5 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEBB2D802BBAAA3600864EF5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEBB2D8A2BBAAA3600864EF5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + DEBB2D6E2BBAAA3500864EF5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DEBB2DA52BBAAAFD00864EF5 /* ViewModel.swift in Sources */, + DEBB2DB22BBAAD0000864EF5 /* SherpaOnnx.swift in Sources */, + DEBB2DA12BBAAAD800864EF5 /* Extension.swift in Sources */, + DEBB2D782BBAAA3500864EF5 /* ContentView.swift in Sources */, + DEBB2D762BBAAA3500864EF5 /* SherpaOnnxLangIDApp.swift in Sources */, + DEBB2DA32BBAAAE700864EF5 /* Model.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEBB2D7E2BBAAA3600864EF5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DEBB2D872BBAAA3600864EF5 /* SherpaOnnxLangIDTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEBB2D882BBAAA3600864EF5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DEBB2D912BBAAA3600864EF5 /* SherpaOnnxLangIDUITests.swift in Sources */, + DEBB2D932BBAAA3600864EF5 /* SherpaOnnxLangIDUITestsLaunchTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + DEBB2D842BBAAA3600864EF5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DEBB2D712BBAAA3500864EF5 /* SherpaOnnxLangID */; + targetProxy = DEBB2D832BBAAA3600864EF5 /* PBXContainerItemProxy */; + }; + DEBB2D8E2BBAAA3600864EF5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DEBB2D712BBAAA3500864EF5 /* SherpaOnnxLangID */; + targetProxy = DEBB2D8D2BBAAA3600864EF5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + DEBB2D942BBAAA3600864EF5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + DEBB2D952BBAAA3600864EF5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + DEBB2D972BBAAA3600864EF5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SherpaOnnxLangID/Preview Content\""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + HEADER_SEARCH_PATHS = "${PROJECT_DIR}/../../build-ios/sherpa-onnx.xcframework/Headers/"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Use microphone to record voice"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = "-lc++"; + PRODUCT_BUNDLE_IDENTIFIER = "com.k2-fsa.org.SherpaOnnxLangID"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "${PROJECT_DIR}/../../swift-api-examples/SherpaOnnx-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + DEBB2D982BBAAA3600864EF5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SherpaOnnxLangID/Preview Content\""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + HEADER_SEARCH_PATHS = "${PROJECT_DIR}/../../build-ios/sherpa-onnx.xcframework/Headers/"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Use microphone to record voice"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = "-lc++"; + PRODUCT_BUNDLE_IDENTIFIER = "com.k2-fsa.org.SherpaOnnxLangID"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "${PROJECT_DIR}/../../swift-api-examples/SherpaOnnx-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + DEBB2D9A2BBAAA3600864EF5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 896WS4KUPV; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.k2-fsa.org.SherpaOnnxLangIDTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SherpaOnnxLangID.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SherpaOnnxLangID"; + }; + name = Debug; + }; + DEBB2D9B2BBAAA3600864EF5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 896WS4KUPV; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.k2-fsa.org.SherpaOnnxLangIDTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SherpaOnnxLangID.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SherpaOnnxLangID"; + }; + name = Release; + }; + DEBB2D9D2BBAAA3600864EF5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 896WS4KUPV; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.k2-fsa.org.SherpaOnnxLangIDUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = SherpaOnnxLangID; + }; + name = Debug; + }; + DEBB2D9E2BBAAA3600864EF5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 896WS4KUPV; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.k2-fsa.org.SherpaOnnxLangIDUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = SherpaOnnxLangID; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + DEBB2D6D2BBAAA3500864EF5 /* Build configuration list for PBXProject "SherpaOnnxLangID" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DEBB2D942BBAAA3600864EF5 /* Debug */, + DEBB2D952BBAAA3600864EF5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DEBB2D962BBAAA3600864EF5 /* Build configuration list for PBXNativeTarget "SherpaOnnxLangID" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DEBB2D972BBAAA3600864EF5 /* Debug */, + DEBB2D982BBAAA3600864EF5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DEBB2D992BBAAA3600864EF5 /* Build configuration list for PBXNativeTarget "SherpaOnnxLangIDTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DEBB2D9A2BBAAA3600864EF5 /* Debug */, + DEBB2D9B2BBAAA3600864EF5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DEBB2D9C2BBAAA3600864EF5 /* Build configuration list for PBXNativeTarget "SherpaOnnxLangIDUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DEBB2D9D2BBAAA3600864EF5 /* Debug */, + DEBB2D9E2BBAAA3600864EF5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = DEBB2D6A2BBAAA3500864EF5 /* Project object */; +} diff --git a/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/Assets.xcassets/AccentColor.colorset/Contents.json b/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/Assets.xcassets/AppIcon 1.appiconset/Contents.json b/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/Assets.xcassets/AppIcon 1.appiconset/Contents.json new file mode 100644 index 000000000..94b3d5c0e --- /dev/null +++ b/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/Assets.xcassets/AppIcon 1.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "k2-1024x1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/Assets.xcassets/AppIcon 1.appiconset/k2-1024x1024.png b/ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID/Assets.xcassets/AppIcon 1.appiconset/k2-1024x1024.png new file mode 100644 index 0000000000000000000000000000000000000000..6e8f833e7275aac3967f5e39baf4c17429bb75e6 GIT binary patch literal 421090 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�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�H8J<*;xc)QYzVIVtjikFZ(ITC`I?QOl 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 z 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Ÿ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%0D720P+y*YDxYE>yqLK--NCLZ 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 Date: Wed, 3 Apr 2024 21:31:42 +0800 Subject: [PATCH 02/45] Add HLG decoding for streaming CTC models (#731) --- .github/scripts/test-online-ctc.sh | 22 ++- .github/scripts/test-python.sh | 19 +- .github/workflows/linux.yaml | 15 +- cmake/kaldi-decoder.cmake | 16 +- .../online-zipformer-ctc-hlg-decode-file.py | 172 ++++++++++++++++++ sherpa-onnx/csrc/CMakeLists.txt | 2 + .../csrc/offline-ctc-fst-decoder-config.cc | 11 ++ .../csrc/offline-ctc-fst-decoder-config.h | 1 + sherpa-onnx/csrc/offline-ctc-fst-decoder.cc | 2 +- sherpa-onnx/csrc/offline-recognizer.cc | 6 + sherpa-onnx/csrc/online-ctc-decoder.h | 12 +- .../csrc/online-ctc-fst-decoder-config.cc | 40 ++++ .../csrc/online-ctc-fst-decoder-config.h | 32 ++++ sherpa-onnx/csrc/online-ctc-fst-decoder.cc | 125 +++++++++++++ sherpa-onnx/csrc/online-ctc-fst-decoder.h | 39 ++++ .../csrc/online-ctc-greedy-search-decoder.cc | 3 +- .../csrc/online-ctc-greedy-search-decoder.h | 3 +- sherpa-onnx/csrc/online-recognizer-ctc-impl.h | 50 ++--- sherpa-onnx/csrc/online-recognizer.cc | 28 ++- sherpa-onnx/csrc/online-recognizer.h | 20 +- sherpa-onnx/csrc/online-stream.cc | 27 +++ sherpa-onnx/csrc/online-stream.h | 6 + sherpa-onnx/python/csrc/CMakeLists.txt | 1 + .../csrc/online-ctc-fst-decoder-config.cc | 23 +++ .../csrc/online-ctc-fst-decoder-config.h | 16 ++ sherpa-onnx/python/csrc/online-recognizer.cc | 42 ++--- sherpa-onnx/python/csrc/sherpa-onnx.cc | 2 + .../python/sherpa_onnx/online_recognizer.py | 15 ++ 28 files changed, 668 insertions(+), 82 deletions(-) create mode 100755 python-api-examples/online-zipformer-ctc-hlg-decode-file.py create mode 100644 sherpa-onnx/csrc/online-ctc-fst-decoder-config.cc create mode 100644 sherpa-onnx/csrc/online-ctc-fst-decoder-config.h create mode 100644 sherpa-onnx/csrc/online-ctc-fst-decoder.cc create mode 100644 sherpa-onnx/csrc/online-ctc-fst-decoder.h create mode 100644 sherpa-onnx/python/csrc/online-ctc-fst-decoder-config.cc create mode 100644 sherpa-onnx/python/csrc/online-ctc-fst-decoder-config.h diff --git a/.github/scripts/test-online-ctc.sh b/.github/scripts/test-online-ctc.sh index fa331be6f..7c631dd05 100755 --- a/.github/scripts/test-online-ctc.sh +++ b/.github/scripts/test-online-ctc.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -e +set -ex log() { # This function is from espnet @@ -13,6 +13,26 @@ echo "PATH: $PATH" which $EXE +log "------------------------------------------------------------" +log "Run streaming Zipformer2 CTC HLG decoding " +log "------------------------------------------------------------" +curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +tar xvf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +rm sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +repo=$PWD/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18 +ls -lh $repo +echo "pwd: $PWD" + +$EXE \ + --zipformer2-ctc-model=$repo/ctc-epoch-30-avg-3-chunk-16-left-128.int8.onnx \ + --ctc-graph=$repo/HLG.fst \ + --tokens=$repo/tokens.txt \ + $repo/test_wavs/0.wav \ + $repo/test_wavs/1.wav \ + $repo/test_wavs/8k.wav + +rm -rf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18 + log "------------------------------------------------------------" log "Run streaming Zipformer2 CTC " log "------------------------------------------------------------" diff --git a/.github/scripts/test-python.sh b/.github/scripts/test-python.sh index b454d5310..3604a0059 100755 --- a/.github/scripts/test-python.sh +++ b/.github/scripts/test-python.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -e +set -ex log() { # This function is from espnet @@ -8,6 +8,23 @@ log() { echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" } +log "test streaming zipformer2 ctc HLG decoding" + +curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +tar xvf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +rm sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +repo=sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18 + +python3 ./python-api-examples/online-zipformer-ctc-hlg-decode-file.py \ + --debug 1 \ + --tokens ./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/tokens.txt \ + --graph ./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/HLG.fst \ + --model ./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/ctc-epoch-30-avg-3-chunk-16-left-128.int8.onnx \ + ./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/test_wavs/0.wav + +rm -rf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18 + + mkdir -p /tmp/icefall-models dir=/tmp/icefall-models diff --git a/.github/workflows/linux.yaml b/.github/workflows/linux.yaml index b1f3fa91b..b32362a3d 100644 --- a/.github/workflows/linux.yaml +++ b/.github/workflows/linux.yaml @@ -124,6 +124,14 @@ jobs: name: release-${{ matrix.build_type }}-with-shared-lib-${{ matrix.shared_lib }}-with-tts-${{ matrix.with_tts }} path: build/bin/* + - name: Test online CTC + shell: bash + run: | + export PATH=$PWD/build/bin:$PATH + export EXE=sherpa-onnx + + .github/scripts/test-online-ctc.sh + - name: Test C API shell: bash run: | @@ -149,13 +157,6 @@ jobs: .github/scripts/test-kws.sh - - name: Test online CTC - shell: bash - run: | - export PATH=$PWD/build/bin:$PATH - export EXE=sherpa-onnx - - .github/scripts/test-online-ctc.sh - name: Test offline Whisper if: matrix.build_type != 'Debug' diff --git a/cmake/kaldi-decoder.cmake b/cmake/kaldi-decoder.cmake index 6ebd3f139..99ebf9aa0 100644 --- a/cmake/kaldi-decoder.cmake +++ b/cmake/kaldi-decoder.cmake @@ -1,9 +1,9 @@ function(download_kaldi_decoder) include(FetchContent) - set(kaldi_decoder_URL "https://github.com/k2-fsa/kaldi-decoder/archive/refs/tags/v0.2.4.tar.gz") - set(kaldi_decoder_URL2 "https://hub.nuaa.cf/k2-fsa/kaldi-decoder/archive/refs/tags/v0.2.4.tar.gz") - set(kaldi_decoder_HASH "SHA256=136d96c2f1f8ec44de095205f81a6ce98981cd867fe4ba840f9415a0b58fe601") + set(kaldi_decoder_URL "https://github.com/k2-fsa/kaldi-decoder/archive/refs/tags/v0.2.5.tar.gz") + set(kaldi_decoder_URL2 "https://hub.nuaa.cf/k2-fsa/kaldi-decoder/archive/refs/tags/v0.2.5.tar.gz") + set(kaldi_decoder_HASH "SHA256=f663e58aef31b33cd8086eaa09ff1383628039845f31300b5abef817d8cc2fff") set(KALDI_DECODER_BUILD_PYTHON OFF CACHE BOOL "" FORCE) set(KALDI_DECODER_ENABLE_TESTS OFF CACHE BOOL "" FORCE) @@ -12,11 +12,11 @@ function(download_kaldi_decoder) # If you don't have access to the Internet, # please pre-download kaldi-decoder set(possible_file_locations - $ENV{HOME}/Downloads/kaldi-decoder-0.2.4.tar.gz - ${CMAKE_SOURCE_DIR}/kaldi-decoder-0.2.4.tar.gz - ${CMAKE_BINARY_DIR}/kaldi-decoder-0.2.4.tar.gz - /tmp/kaldi-decoder-0.2.4.tar.gz - /star-fj/fangjun/download/github/kaldi-decoder-0.2.4.tar.gz + $ENV{HOME}/Downloads/kaldi-decoder-0.2.5.tar.gz + ${CMAKE_SOURCE_DIR}/kaldi-decoder-0.2.5.tar.gz + ${CMAKE_BINARY_DIR}/kaldi-decoder-0.2.5.tar.gz + /tmp/kaldi-decoder-0.2.5.tar.gz + /star-fj/fangjun/download/github/kaldi-decoder-0.2.5.tar.gz ) foreach(f IN LISTS possible_file_locations) diff --git a/python-api-examples/online-zipformer-ctc-hlg-decode-file.py b/python-api-examples/online-zipformer-ctc-hlg-decode-file.py new file mode 100755 index 000000000..869840c7c --- /dev/null +++ b/python-api-examples/online-zipformer-ctc-hlg-decode-file.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 + +# This file shows how to use a streaming zipformer CTC model and an HLG +# graph for decoding. +# +# We use the following model as an example +# +""" +wget https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +tar xvf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +rm sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 + +python3 ./python-api-examples/online-zipformer-ctc-hlg-decode-file.py \ + --tokens ./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/tokens.txt \ + --graph ./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/HLG.fst \ + --model ./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/ctc-epoch-30-avg-3-chunk-16-left-128.int8.onnx \ + ./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/test_wavs/0.wav + +""" +# (The above model is from https://github.com/k2-fsa/icefall/pull/1557) + +import argparse +import time +import wave +from pathlib import Path +from typing import List, Tuple + +import numpy as np +import sherpa_onnx + + +def get_args(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--tokens", + type=str, + required=True, + help="Path to tokens.txt", + ) + + parser.add_argument( + "--model", + type=str, + required=True, + help="Path to the ONNX model", + ) + + parser.add_argument( + "--graph", + type=str, + required=True, + help="Path to H.fst, HL.fst, or HLG.fst", + ) + + parser.add_argument( + "--num-threads", + type=int, + default=1, + help="Number of threads for neural network computation", + ) + + parser.add_argument( + "--provider", + type=str, + default="cpu", + help="Valid values: cpu, cuda, coreml", + ) + + parser.add_argument( + "--debug", + type=int, + default=0, + help="Valid values: 1, 0", + ) + + parser.add_argument( + "sound_file", + type=str, + help="The input sound file to decode. It must be of WAVE" + "format with a single channel, and each sample has 16-bit, " + "i.e., int16_t. " + "The sample rate of the file can be arbitrary and does not need to " + "be 16 kHz", + ) + + return parser.parse_args() + + +def assert_file_exists(filename: str): + assert Path(filename).is_file(), ( + f"{filename} does not exist!\n" + "Please refer to " + "https://k2-fsa.github.io/sherpa/onnx/pretrained_models/index.html to download it" + ) + + +def read_wave(wave_filename: str) -> Tuple[np.ndarray, int]: + """ + Args: + wave_filename: + Path to a wave file. It should be single channel and each sample should + be 16-bit. Its sample rate does not need to be 16kHz. + Returns: + Return a tuple containing: + - A 1-D array of dtype np.float32 containing the samples, which are + normalized to the range [-1, 1]. + - sample rate of the wave file + """ + + with wave.open(wave_filename) as f: + assert f.getnchannels() == 1, f.getnchannels() + assert f.getsampwidth() == 2, f.getsampwidth() # it is in bytes + num_samples = f.getnframes() + samples = f.readframes(num_samples) + samples_int16 = np.frombuffer(samples, dtype=np.int16) + samples_float32 = samples_int16.astype(np.float32) + + samples_float32 = samples_float32 / 32768 + return samples_float32, f.getframerate() + + +def main(): + args = get_args() + print(vars(args)) + + assert_file_exists(args.tokens) + assert_file_exists(args.graph) + assert_file_exists(args.model) + + recognizer = sherpa_onnx.OnlineRecognizer.from_zipformer2_ctc( + tokens=args.tokens, + model=args.model, + num_threads=args.num_threads, + provider=args.provider, + sample_rate=16000, + feature_dim=80, + ctc_graph=args.graph, + ) + + wave_filename = args.sound_file + assert_file_exists(wave_filename) + samples, sample_rate = read_wave(wave_filename) + duration = len(samples) / sample_rate + + print("Started") + + start_time = time.time() + s = recognizer.create_stream() + s.accept_waveform(sample_rate, samples) + tail_paddings = np.zeros(int(0.66 * sample_rate), dtype=np.float32) + s.accept_waveform(sample_rate, tail_paddings) + s.input_finished() + while recognizer.is_ready(s): + recognizer.decode_stream(s) + + result = recognizer.get_result(s).lower() + end_time = time.time() + + elapsed_seconds = end_time - start_time + rtf = elapsed_seconds / duration + print(f"num_threads: {args.num_threads}") + print(f"Wave duration: {duration:.3f} s") + print(f"Elapsed time: {elapsed_seconds:.3f} s") + print(f"Real time factor (RTF): {elapsed_seconds:.3f}/{duration:.3f} = {rtf:.3f}") + print(result) + + +if __name__ == "__main__": + main() diff --git a/sherpa-onnx/csrc/CMakeLists.txt b/sherpa-onnx/csrc/CMakeLists.txt index 86dbc12c9..1ebdc6264 100644 --- a/sherpa-onnx/csrc/CMakeLists.txt +++ b/sherpa-onnx/csrc/CMakeLists.txt @@ -51,6 +51,8 @@ set(sources offline-zipformer-ctc-model-config.cc offline-zipformer-ctc-model.cc online-conformer-transducer-model.cc + online-ctc-fst-decoder-config.cc + online-ctc-fst-decoder.cc online-ctc-greedy-search-decoder.cc online-ctc-model.cc online-lm-config.cc diff --git a/sherpa-onnx/csrc/offline-ctc-fst-decoder-config.cc b/sherpa-onnx/csrc/offline-ctc-fst-decoder-config.cc index bd4126685..481ecaef5 100644 --- a/sherpa-onnx/csrc/offline-ctc-fst-decoder-config.cc +++ b/sherpa-onnx/csrc/offline-ctc-fst-decoder-config.cc @@ -7,6 +7,9 @@ #include #include +#include "sherpa-onnx/csrc/file-utils.h" +#include "sherpa-onnx/csrc/macros.h" + namespace sherpa_onnx { std::string OfflineCtcFstDecoderConfig::ToString() const { @@ -29,4 +32,12 @@ void OfflineCtcFstDecoderConfig::Register(ParseOptions *po) { "Decoder max active states. Larger->slower; more accurate"); } +bool OfflineCtcFstDecoderConfig::Validate() const { + if (!graph.empty() && !FileExists(graph)) { + SHERPA_ONNX_LOGE("graph: %s does not exist", graph.c_str()); + return false; + } + return true; +} + } // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/offline-ctc-fst-decoder-config.h b/sherpa-onnx/csrc/offline-ctc-fst-decoder-config.h index 6d7f70aed..b87fe89e6 100644 --- a/sherpa-onnx/csrc/offline-ctc-fst-decoder-config.h +++ b/sherpa-onnx/csrc/offline-ctc-fst-decoder-config.h @@ -24,6 +24,7 @@ struct OfflineCtcFstDecoderConfig { std::string ToString() const; void Register(ParseOptions *po); + bool Validate() const; }; } // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/offline-ctc-fst-decoder.cc b/sherpa-onnx/csrc/offline-ctc-fst-decoder.cc index efee65a72..e54274df4 100644 --- a/sherpa-onnx/csrc/offline-ctc-fst-decoder.cc +++ b/sherpa-onnx/csrc/offline-ctc-fst-decoder.cc @@ -20,7 +20,7 @@ namespace sherpa_onnx { // @param filename Path to a StdVectorFst or StdConstFst graph // @return The caller should free the returned pointer using `delete` to // avoid memory leak. -static fst::Fst *ReadGraph(const std::string &filename) { +fst::Fst *ReadGraph(const std::string &filename) { // read decoding network FST std::ifstream is(filename, std::ios::binary); if (!is.good()) { diff --git a/sherpa-onnx/csrc/offline-recognizer.cc b/sherpa-onnx/csrc/offline-recognizer.cc index 5c10eb3a1..8005cc855 100644 --- a/sherpa-onnx/csrc/offline-recognizer.cc +++ b/sherpa-onnx/csrc/offline-recognizer.cc @@ -67,6 +67,12 @@ bool OfflineRecognizerConfig::Validate() const { return false; } + if (!ctc_fst_decoder_config.graph.empty() && + !ctc_fst_decoder_config.Validate()) { + SHERPA_ONNX_LOGE("Errors in fst_decoder"); + return false; + } + return model_config.Validate(); } diff --git a/sherpa-onnx/csrc/online-ctc-decoder.h b/sherpa-onnx/csrc/online-ctc-decoder.h index 6690e1bb2..28809e39f 100644 --- a/sherpa-onnx/csrc/online-ctc-decoder.h +++ b/sherpa-onnx/csrc/online-ctc-decoder.h @@ -5,12 +5,16 @@ #ifndef SHERPA_ONNX_CSRC_ONLINE_CTC_DECODER_H_ #define SHERPA_ONNX_CSRC_ONLINE_CTC_DECODER_H_ +#include #include +#include "kaldi-decoder/csrc/faster-decoder.h" #include "onnxruntime_cxx_api.h" // NOLINT namespace sherpa_onnx { +class OnlineStream; + struct OnlineCtcDecoderResult { /// Number of frames after subsampling we have decoded so far int32_t frame_offset = 0; @@ -37,7 +41,13 @@ class OnlineCtcDecoder { * @param results Input & Output parameters.. */ virtual void Decode(Ort::Value log_probs, - std::vector *results) = 0; + std::vector *results, + OnlineStream **ss = nullptr, int32_t n = 0) = 0; + + virtual std::unique_ptr CreateFasterDecoder() + const { + return nullptr; + } }; } // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/online-ctc-fst-decoder-config.cc b/sherpa-onnx/csrc/online-ctc-fst-decoder-config.cc new file mode 100644 index 000000000..9eccebea7 --- /dev/null +++ b/sherpa-onnx/csrc/online-ctc-fst-decoder-config.cc @@ -0,0 +1,40 @@ +// sherpa-onnx/csrc/online-ctc-fst-decoder-config.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/csrc/online-ctc-fst-decoder-config.h" + +#include +#include + +#include "sherpa-onnx/csrc/file-utils.h" +#include "sherpa-onnx/csrc/macros.h" + +namespace sherpa_onnx { + +std::string OnlineCtcFstDecoderConfig::ToString() const { + std::ostringstream os; + + os << "OnlineCtcFstDecoderConfig("; + os << "graph=\"" << graph << "\", "; + os << "max_active=" << max_active << ")"; + + return os.str(); +} + +void OnlineCtcFstDecoderConfig::Register(ParseOptions *po) { + po->Register("ctc-graph", &graph, "Path to H.fst, HL.fst, or HLG.fst"); + + po->Register("ctc-max-active", &max_active, + "Decoder max active states. Larger->slower; more accurate"); +} + +bool OnlineCtcFstDecoderConfig::Validate() const { + if (!graph.empty() && !FileExists(graph)) { + SHERPA_ONNX_LOGE("graph: %s does not exist", graph.c_str()); + return false; + } + return true; +} + +} // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/online-ctc-fst-decoder-config.h b/sherpa-onnx/csrc/online-ctc-fst-decoder-config.h new file mode 100644 index 000000000..6f9e5b156 --- /dev/null +++ b/sherpa-onnx/csrc/online-ctc-fst-decoder-config.h @@ -0,0 +1,32 @@ +// sherpa-onnx/csrc/online-ctc-fst-decoder-config.h +// +// Copyright (c) 2024 Xiaomi Corporation + +#ifndef SHERPA_ONNX_CSRC_ONLINE_CTC_FST_DECODER_CONFIG_H_ +#define SHERPA_ONNX_CSRC_ONLINE_CTC_FST_DECODER_CONFIG_H_ + +#include + +#include "sherpa-onnx/csrc/parse-options.h" + +namespace sherpa_onnx { + +struct OnlineCtcFstDecoderConfig { + // Path to H.fst, HL.fst or HLG.fst + std::string graph; + int32_t max_active = 3000; + + OnlineCtcFstDecoderConfig() = default; + + OnlineCtcFstDecoderConfig(const std::string &graph, int32_t max_active) + : graph(graph), max_active(max_active) {} + + std::string ToString() const; + + void Register(ParseOptions *po); + bool Validate() const; +}; + +} // namespace sherpa_onnx + +#endif // SHERPA_ONNX_CSRC_ONLINE_CTC_FST_DECODER_CONFIG_H_ diff --git a/sherpa-onnx/csrc/online-ctc-fst-decoder.cc b/sherpa-onnx/csrc/online-ctc-fst-decoder.cc new file mode 100644 index 000000000..7619e0db5 --- /dev/null +++ b/sherpa-onnx/csrc/online-ctc-fst-decoder.cc @@ -0,0 +1,125 @@ +// sherpa-onnx/csrc/online-ctc-fst-decoder.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/csrc/online-ctc-fst-decoder.h" + +#include +#include +#include +#include +#include + +#include "fst/fstlib.h" +#include "kaldi-decoder/csrc/decodable-ctc.h" +#include "kaldifst/csrc/fstext-utils.h" +#include "sherpa-onnx/csrc/macros.h" +#include "sherpa-onnx/csrc/online-stream.h" + +namespace sherpa_onnx { + +// defined in ./offline-ctc-fst-decoder.cc +fst::Fst *ReadGraph(const std::string &filename); + +OnlineCtcFstDecoder::OnlineCtcFstDecoder( + const OnlineCtcFstDecoderConfig &config, int32_t blank_id) + : config_(config), fst_(ReadGraph(config.graph)), blank_id_(blank_id) { + options_.max_active = config_.max_active; +} + +std::unique_ptr +OnlineCtcFstDecoder::CreateFasterDecoder() const { + return std::make_unique(*fst_, options_); +} + +static void DecodeOne(const float *log_probs, int32_t num_rows, + int32_t num_cols, OnlineCtcDecoderResult *result, + OnlineStream *s, int32_t blank_id) { + int32_t &processed_frames = s->GetFasterDecoderProcessedFrames(); + kaldi_decoder::DecodableCtc decodable(log_probs, num_rows, num_cols, + processed_frames); + + kaldi_decoder::FasterDecoder *decoder = s->GetFasterDecoder(); + if (processed_frames == 0) { + decoder->InitDecoding(); + } + + decoder->AdvanceDecoding(&decodable); + + if (decoder->ReachedFinal()) { + fst::VectorFst fst_out; + bool ok = decoder->GetBestPath(&fst_out); + if (ok) { + std::vector isymbols_out; + std::vector osymbols_out_unused; + ok = fst::GetLinearSymbolSequence(fst_out, &isymbols_out, + &osymbols_out_unused, nullptr); + std::vector tokens; + tokens.reserve(isymbols_out.size()); + + std::vector timestamps; + timestamps.reserve(isymbols_out.size()); + + std::ostringstream os; + int32_t prev_id = -1; + int32_t num_trailing_blanks = 0; + int32_t f = 0; // frame number + + for (auto i : isymbols_out) { + i -= 1; + + if (i == blank_id) { + num_trailing_blanks += 1; + } else { + num_trailing_blanks = 0; + } + + if (i != blank_id && i != prev_id) { + tokens.push_back(i); + timestamps.push_back(f); + } + prev_id = i; + f += 1; + } + + result->tokens = std::move(tokens); + result->timestamps = std::move(timestamps); + // no need to set frame_offset + } + } + + processed_frames += num_rows; +} + +void OnlineCtcFstDecoder::Decode(Ort::Value log_probs, + std::vector *results, + OnlineStream **ss, int32_t n) { + std::vector log_probs_shape = + log_probs.GetTensorTypeAndShapeInfo().GetShape(); + + if (log_probs_shape[0] != results->size()) { + SHERPA_ONNX_LOGE("Size mismatch! log_probs.size(0) %d, results.size(0): %d", + static_cast(log_probs_shape[0]), + static_cast(results->size())); + exit(-1); + } + + if (log_probs_shape[0] != n) { + SHERPA_ONNX_LOGE("Size mismatch! log_probs.size(0) %d, n: %d", + static_cast(log_probs_shape[0]), n); + exit(-1); + } + + int32_t batch_size = static_cast(log_probs_shape[0]); + int32_t num_frames = static_cast(log_probs_shape[1]); + int32_t vocab_size = static_cast(log_probs_shape[2]); + + const float *p = log_probs.GetTensorData(); + + for (int32_t i = 0; i != batch_size; ++i) { + DecodeOne(p + i * num_frames * vocab_size, num_frames, vocab_size, + &(*results)[i], ss[i], blank_id_); + } +} + +} // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/online-ctc-fst-decoder.h b/sherpa-onnx/csrc/online-ctc-fst-decoder.h new file mode 100644 index 000000000..992276d6b --- /dev/null +++ b/sherpa-onnx/csrc/online-ctc-fst-decoder.h @@ -0,0 +1,39 @@ +// sherpa-onnx/csrc/online-ctc-fst-decoder.h +// +// Copyright (c) 2024 Xiaomi Corporation + +#ifndef SHERPA_ONNX_CSRC_ONLINE_CTC_FST_DECODER_H_ +#define SHERPA_ONNX_CSRC_ONLINE_CTC_FST_DECODER_H_ + +#include +#include + +#include "fst/fst.h" +#include "sherpa-onnx/csrc/online-ctc-decoder.h" +#include "sherpa-onnx/csrc/online-ctc-fst-decoder-config.h" + +namespace sherpa_onnx { + +class OnlineCtcFstDecoder : public OnlineCtcDecoder { + public: + OnlineCtcFstDecoder(const OnlineCtcFstDecoderConfig &config, + int32_t blank_id); + + void Decode(Ort::Value log_probs, + std::vector *results, + OnlineStream **ss = nullptr, int32_t n = 0) override; + + std::unique_ptr CreateFasterDecoder() + const override; + + private: + OnlineCtcFstDecoderConfig config_; + kaldi_decoder::FasterDecoderOptions options_; + + std::unique_ptr> fst_; + int32_t blank_id_ = 0; +}; + +} // namespace sherpa_onnx + +#endif // SHERPA_ONNX_CSRC_ONLINE_CTC_FST_DECODER_H_ diff --git a/sherpa-onnx/csrc/online-ctc-greedy-search-decoder.cc b/sherpa-onnx/csrc/online-ctc-greedy-search-decoder.cc index 909373e71..e813c9873 100644 --- a/sherpa-onnx/csrc/online-ctc-greedy-search-decoder.cc +++ b/sherpa-onnx/csrc/online-ctc-greedy-search-decoder.cc @@ -13,7 +13,8 @@ namespace sherpa_onnx { void OnlineCtcGreedySearchDecoder::Decode( - Ort::Value log_probs, std::vector *results) { + Ort::Value log_probs, std::vector *results, + OnlineStream ** /*ss=nullptr*/, int32_t /*n = 0*/) { std::vector log_probs_shape = log_probs.GetTensorTypeAndShapeInfo().GetShape(); diff --git a/sherpa-onnx/csrc/online-ctc-greedy-search-decoder.h b/sherpa-onnx/csrc/online-ctc-greedy-search-decoder.h index fc724f2c3..0af37593e 100644 --- a/sherpa-onnx/csrc/online-ctc-greedy-search-decoder.h +++ b/sherpa-onnx/csrc/online-ctc-greedy-search-decoder.h @@ -17,7 +17,8 @@ class OnlineCtcGreedySearchDecoder : public OnlineCtcDecoder { : blank_id_(blank_id) {} void Decode(Ort::Value log_probs, - std::vector *results) override; + std::vector *results, + OnlineStream **ss = nullptr, int32_t n = 0) override; private: int32_t blank_id_; diff --git a/sherpa-onnx/csrc/online-recognizer-ctc-impl.h b/sherpa-onnx/csrc/online-recognizer-ctc-impl.h index 5697a77e8..4b137e299 100644 --- a/sherpa-onnx/csrc/online-recognizer-ctc-impl.h +++ b/sherpa-onnx/csrc/online-recognizer-ctc-impl.h @@ -16,6 +16,7 @@ #include "sherpa-onnx/csrc/file-utils.h" #include "sherpa-onnx/csrc/macros.h" #include "sherpa-onnx/csrc/online-ctc-decoder.h" +#include "sherpa-onnx/csrc/online-ctc-fst-decoder.h" #include "sherpa-onnx/csrc/online-ctc-greedy-search-decoder.h" #include "sherpa-onnx/csrc/online-ctc-model.h" #include "sherpa-onnx/csrc/online-recognizer-impl.h" @@ -99,6 +100,7 @@ class OnlineRecognizerCtcImpl : public OnlineRecognizerImpl { std::unique_ptr CreateStream() const override { auto stream = std::make_unique(config_.feat_config); stream->SetStates(model_->GetInitStates()); + stream->SetFasterDecoder(decoder_->CreateFasterDecoder()); return stream; } @@ -165,7 +167,7 @@ class OnlineRecognizerCtcImpl : public OnlineRecognizerImpl { std::vector> next_states = model_->UnStackStates(std::move(out_states)); - decoder_->Decode(std::move(out[0]), &results); + decoder_->Decode(std::move(out[0]), &results, ss, n); for (int32_t k = 0; k != n; ++k) { ss[k]->SetCtcResult(results[k]); @@ -221,30 +223,34 @@ class OnlineRecognizerCtcImpl : public OnlineRecognizerImpl { private: void InitDecoder() { - if (config_.decoding_method == "greedy_search") { - if (!sym_.contains("") && !sym_.contains("") && - !sym_.contains("")) { - SHERPA_ONNX_LOGE( - "We expect that tokens.txt contains " - "the symbol or or and its ID."); - exit(-1); - } + if (!sym_.contains("") && !sym_.contains("") && + !sym_.contains("")) { + SHERPA_ONNX_LOGE( + "We expect that tokens.txt contains " + "the symbol or or and its ID."); + exit(-1); + } - int32_t blank_id = 0; - if (sym_.contains("")) { - blank_id = sym_[""]; - } else if (sym_.contains("")) { - // for tdnn models of the yesno recipe from icefall - blank_id = sym_[""]; - } else if (sym_.contains("")) { - // for WeNet CTC models - blank_id = sym_[""]; - } + int32_t blank_id = 0; + if (sym_.contains("")) { + blank_id = sym_[""]; + } else if (sym_.contains("")) { + // for tdnn models of the yesno recipe from icefall + blank_id = sym_[""]; + } else if (sym_.contains("")) { + // for WeNet CTC models + blank_id = sym_[""]; + } + if (!config_.ctc_fst_decoder_config.graph.empty()) { + decoder_ = std::make_unique( + config_.ctc_fst_decoder_config, blank_id); + } else if (config_.decoding_method == "greedy_search") { decoder_ = std::make_unique(blank_id); } else { - SHERPA_ONNX_LOGE("Unsupported decoding method: %s", - config_.decoding_method.c_str()); + SHERPA_ONNX_LOGE( + "Unsupported decoding method: %s for streaming CTC models", + config_.decoding_method.c_str()); exit(-1); } } @@ -281,7 +287,7 @@ class OnlineRecognizerCtcImpl : public OnlineRecognizerImpl { std::vector results(1); results[0] = std::move(s->GetCtcResult()); - decoder_->Decode(std::move(out[0]), &results); + decoder_->Decode(std::move(out[0]), &results, &s, 1); s->SetCtcResult(results[0]); } diff --git a/sherpa-onnx/csrc/online-recognizer.cc b/sherpa-onnx/csrc/online-recognizer.cc index ea7e9f905..5d3445659 100644 --- a/sherpa-onnx/csrc/online-recognizer.cc +++ b/sherpa-onnx/csrc/online-recognizer.cc @@ -19,13 +19,13 @@ namespace sherpa_onnx { /// Helper for `OnlineRecognizerResult::AsJsonString()` -template -std::string VecToString(const std::vector& vec, int32_t precision = 6) { +template +std::string VecToString(const std::vector &vec, int32_t precision = 6) { std::ostringstream oss; oss << std::fixed << std::setprecision(precision); oss << "[ "; std::string sep = ""; - for (const auto& item : vec) { + for (const auto &item : vec) { oss << sep << item; sep = ", "; } @@ -34,13 +34,13 @@ std::string VecToString(const std::vector& vec, int32_t precision = 6) { } /// Helper for `OnlineRecognizerResult::AsJsonString()` -template<> // explicit specialization for T = std::string -std::string VecToString(const std::vector& vec, +template <> // explicit specialization for T = std::string +std::string VecToString(const std::vector &vec, int32_t) { // ignore 2nd arg std::ostringstream oss; oss << "[ "; std::string sep = ""; - for (const auto& item : vec) { + for (const auto &item : vec) { oss << sep << "\"" << item << "\""; sep = ", "; } @@ -51,15 +51,17 @@ std::string VecToString(const std::vector& vec, std::string OnlineRecognizerResult::AsJsonString() const { std::ostringstream os; os << "{ "; - os << "\"text\": " << "\"" << text << "\"" << ", "; + os << "\"text\": " + << "\"" << text << "\"" + << ", "; os << "\"tokens\": " << VecToString(tokens) << ", "; os << "\"timestamps\": " << VecToString(timestamps, 2) << ", "; os << "\"ys_probs\": " << VecToString(ys_probs, 6) << ", "; os << "\"lm_probs\": " << VecToString(lm_probs, 6) << ", "; os << "\"context_scores\": " << VecToString(context_scores, 6) << ", "; os << "\"segment\": " << segment << ", "; - os << "\"start_time\": " << std::fixed << std::setprecision(2) - << start_time << ", "; + os << "\"start_time\": " << std::fixed << std::setprecision(2) << start_time + << ", "; os << "\"is_final\": " << (is_final ? "true" : "false"); os << "}"; return os.str(); @@ -70,6 +72,7 @@ void OnlineRecognizerConfig::Register(ParseOptions *po) { model_config.Register(po); endpoint_config.Register(po); lm_config.Register(po); + ctc_fst_decoder_config.Register(po); po->Register("enable-endpoint", &enable_endpoint, "True to enable endpoint detection. False to disable it."); @@ -116,6 +119,12 @@ bool OnlineRecognizerConfig::Validate() const { return false; } + if (!ctc_fst_decoder_config.graph.empty() && + !ctc_fst_decoder_config.Validate()) { + SHERPA_ONNX_LOGE("Errors in ctc_fst_decoder_config"); + return false; + } + return model_config.Validate(); } @@ -127,6 +136,7 @@ std::string OnlineRecognizerConfig::ToString() const { os << "model_config=" << model_config.ToString() << ", "; os << "lm_config=" << lm_config.ToString() << ", "; os << "endpoint_config=" << endpoint_config.ToString() << ", "; + os << "ctc_fst_decoder_config=" << ctc_fst_decoder_config.ToString() << ", "; os << "enable_endpoint=" << (enable_endpoint ? "True" : "False") << ", "; os << "max_active_paths=" << max_active_paths << ", "; os << "hotwords_score=" << hotwords_score << ", "; diff --git a/sherpa-onnx/csrc/online-recognizer.h b/sherpa-onnx/csrc/online-recognizer.h index ec8875e68..e7f1b38d7 100644 --- a/sherpa-onnx/csrc/online-recognizer.h +++ b/sherpa-onnx/csrc/online-recognizer.h @@ -16,6 +16,7 @@ #include "sherpa-onnx/csrc/endpoint.h" #include "sherpa-onnx/csrc/features.h" +#include "sherpa-onnx/csrc/online-ctc-fst-decoder-config.h" #include "sherpa-onnx/csrc/online-lm-config.h" #include "sherpa-onnx/csrc/online-model-config.h" #include "sherpa-onnx/csrc/online-stream.h" @@ -80,6 +81,7 @@ struct OnlineRecognizerConfig { OnlineModelConfig model_config; OnlineLMConfig lm_config; EndpointConfig endpoint_config; + OnlineCtcFstDecoderConfig ctc_fst_decoder_config; bool enable_endpoint = true; std::string decoding_method = "greedy_search"; @@ -96,19 +98,19 @@ struct OnlineRecognizerConfig { OnlineRecognizerConfig() = default; - OnlineRecognizerConfig(const FeatureExtractorConfig &feat_config, - const OnlineModelConfig &model_config, - const OnlineLMConfig &lm_config, - const EndpointConfig &endpoint_config, - bool enable_endpoint, - const std::string &decoding_method, - int32_t max_active_paths, - const std::string &hotwords_file, float hotwords_score, - float blank_penalty) + OnlineRecognizerConfig( + const FeatureExtractorConfig &feat_config, + const OnlineModelConfig &model_config, const OnlineLMConfig &lm_config, + const EndpointConfig &endpoint_config, + const OnlineCtcFstDecoderConfig &ctc_fst_decoder_config, + bool enable_endpoint, const std::string &decoding_method, + int32_t max_active_paths, const std::string &hotwords_file, + float hotwords_score, float blank_penalty) : feat_config(feat_config), model_config(model_config), lm_config(lm_config), endpoint_config(endpoint_config), + ctc_fst_decoder_config(ctc_fst_decoder_config), enable_endpoint(enable_endpoint), decoding_method(decoding_method), max_active_paths(max_active_paths), diff --git a/sherpa-onnx/csrc/online-stream.cc b/sherpa-onnx/csrc/online-stream.cc index aaddfb545..52cfb899f 100644 --- a/sherpa-onnx/csrc/online-stream.cc +++ b/sherpa-onnx/csrc/online-stream.cc @@ -104,6 +104,18 @@ class OnlineStream::Impl { return paraformer_alpha_cache_; } + void SetFasterDecoder(std::unique_ptr decoder) { + faster_decoder_ = std::move(decoder); + } + + kaldi_decoder::FasterDecoder *GetFasterDecoder() const { + return faster_decoder_.get(); + } + + int32_t &GetFasterDecoderProcessedFrames() { + return faster_decoder_processed_frames_; + } + private: FeatureExtractor feat_extractor_; /// For contextual-biasing @@ -121,6 +133,8 @@ class OnlineStream::Impl { std::vector paraformer_encoder_out_cache_; std::vector paraformer_alpha_cache_; OnlineParaformerDecoderResult paraformer_result_; + std::unique_ptr faster_decoder_; + int32_t faster_decoder_processed_frames_ = 0; }; OnlineStream::OnlineStream(const FeatureExtractorConfig &config /*= {}*/, @@ -208,6 +222,19 @@ const ContextGraphPtr &OnlineStream::GetContextGraph() const { return impl_->GetContextGraph(); } +void OnlineStream::SetFasterDecoder( + std::unique_ptr decoder) { + impl_->SetFasterDecoder(std::move(decoder)); +} + +kaldi_decoder::FasterDecoder *OnlineStream::GetFasterDecoder() const { + return impl_->GetFasterDecoder(); +} + +int32_t &OnlineStream::GetFasterDecoderProcessedFrames() { + return impl_->GetFasterDecoderProcessedFrames(); +} + std::vector &OnlineStream::GetParaformerFeatCache() { return impl_->GetParaformerFeatCache(); } diff --git a/sherpa-onnx/csrc/online-stream.h b/sherpa-onnx/csrc/online-stream.h index f648ca5dc..49b7f7402 100644 --- a/sherpa-onnx/csrc/online-stream.h +++ b/sherpa-onnx/csrc/online-stream.h @@ -8,6 +8,7 @@ #include #include +#include "kaldi-decoder/csrc/faster-decoder.h" #include "onnxruntime_cxx_api.h" // NOLINT #include "sherpa-onnx/csrc/context-graph.h" #include "sherpa-onnx/csrc/features.h" @@ -97,6 +98,11 @@ class OnlineStream { */ const ContextGraphPtr &GetContextGraph() const; + // for online ctc decoder + void SetFasterDecoder(std::unique_ptr decoder); + kaldi_decoder::FasterDecoder *GetFasterDecoder() const; + int32_t &GetFasterDecoderProcessedFrames(); + // for streaming paraformer std::vector &GetParaformerFeatCache(); std::vector &GetParaformerEncoderOutCache(); diff --git a/sherpa-onnx/python/csrc/CMakeLists.txt b/sherpa-onnx/python/csrc/CMakeLists.txt index 9e5af779d..53aebd78c 100644 --- a/sherpa-onnx/python/csrc/CMakeLists.txt +++ b/sherpa-onnx/python/csrc/CMakeLists.txt @@ -18,6 +18,7 @@ set(srcs offline-wenet-ctc-model-config.cc offline-whisper-model-config.cc offline-zipformer-ctc-model-config.cc + online-ctc-fst-decoder-config.cc online-lm-config.cc online-model-config.cc online-paraformer-model-config.cc diff --git a/sherpa-onnx/python/csrc/online-ctc-fst-decoder-config.cc b/sherpa-onnx/python/csrc/online-ctc-fst-decoder-config.cc new file mode 100644 index 000000000..116278ec0 --- /dev/null +++ b/sherpa-onnx/python/csrc/online-ctc-fst-decoder-config.cc @@ -0,0 +1,23 @@ +// sherpa-onnx/python/csrc/online-ctc-fst-decoder-config.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/python/csrc/online-ctc-fst-decoder-config.h" + +#include + +#include "sherpa-onnx/csrc/online-ctc-fst-decoder-config.h" + +namespace sherpa_onnx { + +void PybindOnlineCtcFstDecoderConfig(py::module *m) { + using PyClass = OnlineCtcFstDecoderConfig; + py::class_(*m, "OnlineCtcFstDecoderConfig") + .def(py::init(), py::arg("graph") = "", + py::arg("max_active") = 3000) + .def_readwrite("graph", &PyClass::graph) + .def_readwrite("max_active", &PyClass::max_active) + .def("__str__", &PyClass::ToString); +} + +} // namespace sherpa_onnx diff --git a/sherpa-onnx/python/csrc/online-ctc-fst-decoder-config.h b/sherpa-onnx/python/csrc/online-ctc-fst-decoder-config.h new file mode 100644 index 000000000..00727646b --- /dev/null +++ b/sherpa-onnx/python/csrc/online-ctc-fst-decoder-config.h @@ -0,0 +1,16 @@ +// sherpa-onnx/python/csrc/online-ctc-fst-decoder-config.h +// +// Copyright (c) 2024 Xiaomi Corporation + +#ifndef SHERPA_ONNX_PYTHON_CSRC_ONLINE_CTC_FST_DECODER_CONFIG_H_ +#define SHERPA_ONNX_PYTHON_CSRC_ONLINE_CTC_FST_DECODER_CONFIG_H_ + +#include "sherpa-onnx/python/csrc/sherpa-onnx.h" + +namespace sherpa_onnx { + +void PybindOnlineCtcFstDecoderConfig(py::module *m); + +} + +#endif // SHERPA_ONNX_PYTHON_CSRC_ONLINE_CTC_FST_DECODER_CONFIG_H_ diff --git a/sherpa-onnx/python/csrc/online-recognizer.cc b/sherpa-onnx/python/csrc/online-recognizer.cc index 0213bd7b2..bd98c94e2 100644 --- a/sherpa-onnx/python/csrc/online-recognizer.cc +++ b/sherpa-onnx/python/csrc/online-recognizer.cc @@ -24,8 +24,7 @@ static void PybindOnlineRecognizerResult(py::module *m) { "tokens", [](PyClass &self) -> std::vector { return self.tokens; }) .def_property_readonly( - "start_time", - [](PyClass &self) -> float { return self.start_time; }) + "start_time", [](PyClass &self) -> float { return self.start_time; }) .def_property_readonly( "timestamps", [](PyClass &self) -> std::vector { return self.timestamps; }) @@ -35,37 +34,38 @@ static void PybindOnlineRecognizerResult(py::module *m) { .def_property_readonly( "lm_probs", [](PyClass &self) -> std::vector { return self.lm_probs; }) + .def_property_readonly("context_scores", + [](PyClass &self) -> std::vector { + return self.context_scores; + }) .def_property_readonly( - "context_scores", - [](PyClass &self) -> std::vector { - return self.context_scores; - }) + "segment", [](PyClass &self) -> int32_t { return self.segment; }) .def_property_readonly( - "segment", - [](PyClass &self) -> int32_t { return self.segment; }) - .def_property_readonly( - "is_final", - [](PyClass &self) -> bool { return self.is_final; }) + "is_final", [](PyClass &self) -> bool { return self.is_final; }) .def("as_json_string", &PyClass::AsJsonString, - py::call_guard()); + py::call_guard()); } static void PybindOnlineRecognizerConfig(py::module *m) { using PyClass = OnlineRecognizerConfig; py::class_(*m, "OnlineRecognizerConfig") - .def(py::init(), - py::arg("feat_config"), py::arg("model_config"), - py::arg("lm_config") = OnlineLMConfig(), py::arg("endpoint_config"), - py::arg("enable_endpoint"), py::arg("decoding_method"), - py::arg("max_active_paths") = 4, py::arg("hotwords_file") = "", - py::arg("hotwords_score") = 0, py::arg("blank_penalty") = 0.0) + .def( + py::init(), + py::arg("feat_config"), py::arg("model_config"), + py::arg("lm_config") = OnlineLMConfig(), + py::arg("endpoint_config") = EndpointConfig(), + py::arg("ctc_fst_decoder_config") = OnlineCtcFstDecoderConfig(), + py::arg("enable_endpoint"), py::arg("decoding_method"), + py::arg("max_active_paths") = 4, py::arg("hotwords_file") = "", + py::arg("hotwords_score") = 0, py::arg("blank_penalty") = 0.0) .def_readwrite("feat_config", &PyClass::feat_config) .def_readwrite("model_config", &PyClass::model_config) .def_readwrite("lm_config", &PyClass::lm_config) .def_readwrite("endpoint_config", &PyClass::endpoint_config) + .def_readwrite("ctc_fst_decoder_config", &PyClass::ctc_fst_decoder_config) .def_readwrite("enable_endpoint", &PyClass::enable_endpoint) .def_readwrite("decoding_method", &PyClass::decoding_method) .def_readwrite("max_active_paths", &PyClass::max_active_paths) diff --git a/sherpa-onnx/python/csrc/sherpa-onnx.cc b/sherpa-onnx/python/csrc/sherpa-onnx.cc index 62c64ec72..4952e150b 100644 --- a/sherpa-onnx/python/csrc/sherpa-onnx.cc +++ b/sherpa-onnx/python/csrc/sherpa-onnx.cc @@ -15,6 +15,7 @@ #include "sherpa-onnx/python/csrc/offline-model-config.h" #include "sherpa-onnx/python/csrc/offline-recognizer.h" #include "sherpa-onnx/python/csrc/offline-stream.h" +#include "sherpa-onnx/python/csrc/online-ctc-fst-decoder-config.h" #include "sherpa-onnx/python/csrc/online-lm-config.h" #include "sherpa-onnx/python/csrc/online-model-config.h" #include "sherpa-onnx/python/csrc/online-recognizer.h" @@ -36,6 +37,7 @@ PYBIND11_MODULE(_sherpa_onnx, m) { m.doc() = "pybind11 binding of sherpa-onnx"; PybindFeatures(&m); + PybindOnlineCtcFstDecoderConfig(&m); PybindOnlineModelConfig(&m); PybindOnlineLMConfig(&m); PybindOnlineStream(&m); diff --git a/sherpa-onnx/python/sherpa_onnx/online_recognizer.py b/sherpa-onnx/python/sherpa_onnx/online_recognizer.py index 105043399..a82ab1703 100644 --- a/sherpa-onnx/python/sherpa_onnx/online_recognizer.py +++ b/sherpa-onnx/python/sherpa_onnx/online_recognizer.py @@ -16,6 +16,7 @@ OnlineTransducerModelConfig, OnlineWenetCtcModelConfig, OnlineZipformer2CtcModelConfig, + OnlineCtcFstDecoderConfig, ) @@ -314,6 +315,8 @@ def from_zipformer2_ctc( rule2_min_trailing_silence: float = 1.2, rule3_min_utterance_length: float = 20.0, decoding_method: str = "greedy_search", + ctc_graph: str = "", + ctc_max_active: int = 3000, provider: str = "cpu", ): """ @@ -355,6 +358,12 @@ def from_zipformer2_ctc( is detected. decoding_method: The only valid value is greedy_search. + ctc_graph: + If not empty, decoding_method is ignored. It contains the path to + H.fst, HL.fst, or HLG.fst + ctc_max_active: + Used only when ctc_graph is not empty. It specifies the maximum + active paths at a time. provider: onnxruntime execution providers. Valid values are: cpu, cuda, coreml. """ @@ -384,10 +393,16 @@ def from_zipformer2_ctc( rule3_min_utterance_length=rule3_min_utterance_length, ) + ctc_fst_decoder_config = OnlineCtcFstDecoderConfig( + graph=ctc_graph, + max_active=ctc_max_active, + ) + recognizer_config = OnlineRecognizerConfig( feat_config=feat_config, model_config=model_config, endpoint_config=endpoint_config, + ctc_fst_decoder_config=ctc_fst_decoder_config, enable_endpoint=enable_endpoint_detection, decoding_method=decoding_method, ) From dbff2eaadba78729c7660c0cd0cbf2f5f0252007 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Fri, 5 Apr 2024 10:31:20 +0800 Subject: [PATCH 03/45] Add C API for streaming HLG decoding (#734) --- .github/scripts/test-dot-net.sh | 5 +- .github/scripts/test-nodejs-npm.sh | 7 + .github/scripts/test-swift.sh | 5 + .github/workflows/test-dot-net.yaml | 1 + .github/workflows/test-go-package.yaml | 67 ++++++++- .github/workflows/test-go.yaml | 6 + c-api-examples/CMakeLists.txt | 3 + .../streaming-hlg-decode-file-c-api.c | 130 ++++++++++++++++++ cmake/onnxruntime.cmake | 2 +- dotnet-examples/sherpa-onnx.sln | 6 + .../streaming-hlg-decoding/Program.cs | 66 +++++++++ .../streaming-hlg-decoding/WaveReader.cs | 1 + dotnet-examples/streaming-hlg-decoding/run.sh | 11 ++ .../streaming-hlg-decoding.csproj | 15 ++ go-api-examples/streaming-hlg-decoding/go.mod | 3 + .../streaming-hlg-decoding/main.go | 109 +++++++++++++++ go-api-examples/streaming-hlg-decoding/run.sh | 14 ++ nodejs-examples/README.md | 13 ++ .../test-online-paraformer-microphone.js | 4 + nodejs-examples/test-online-paraformer.js | 4 + .../test-online-transducer-microphone.js | 4 + nodejs-examples/test-online-transducer.js | 4 + .../test-online-zipformer2-ctc-hlg.js | 125 +++++++++++++++++ nodejs-examples/test-online-zipformer2-ctc.js | 4 + .../examples/streaming-hlg-decoding.csproj | 19 +++ scripts/dotnet/online.cs | 18 +++ .../streaming-hlg-decoding/.gitignore | 1 + .../_internal/streaming-hlg-decoding/go.mod | 5 + .../_internal/streaming-hlg-decoding/main.go | 1 + .../_internal/streaming-hlg-decoding/run.sh | 1 + scripts/go/sherpa_onnx.go | 10 ++ sherpa-onnx/c-api/c-api.cc | 5 + sherpa-onnx/c-api/c-api.h | 7 + swift-api-examples/.gitignore | 1 + swift-api-examples/SherpaOnnx.swift | 16 ++- .../run-streaming-hlg-decode-file.sh | 36 +++++ .../streaming-hlg-decode-file.swift | 79 +++++++++++ wasm/asr/sherpa-onnx-asr.js | 30 +++- wasm/asr/sherpa-onnx-wasm-main-asr.cc | 9 +- 39 files changed, 839 insertions(+), 8 deletions(-) create mode 100644 c-api-examples/streaming-hlg-decode-file-c-api.c create mode 100644 dotnet-examples/streaming-hlg-decoding/Program.cs create mode 120000 dotnet-examples/streaming-hlg-decoding/WaveReader.cs create mode 100755 dotnet-examples/streaming-hlg-decoding/run.sh create mode 100644 dotnet-examples/streaming-hlg-decoding/streaming-hlg-decoding.csproj create mode 100644 go-api-examples/streaming-hlg-decoding/go.mod create mode 100644 go-api-examples/streaming-hlg-decoding/main.go create mode 100755 go-api-examples/streaming-hlg-decoding/run.sh create mode 100644 nodejs-examples/test-online-zipformer2-ctc-hlg.js create mode 100644 scripts/dotnet/examples/streaming-hlg-decoding.csproj create mode 100644 scripts/go/_internal/streaming-hlg-decoding/.gitignore create mode 100644 scripts/go/_internal/streaming-hlg-decoding/go.mod create mode 120000 scripts/go/_internal/streaming-hlg-decoding/main.go create mode 120000 scripts/go/_internal/streaming-hlg-decoding/run.sh create mode 100755 swift-api-examples/run-streaming-hlg-decode-file.sh create mode 100644 swift-api-examples/streaming-hlg-decode-file.swift diff --git a/.github/scripts/test-dot-net.sh b/.github/scripts/test-dot-net.sh index c5c6d5a40..b757781c3 100755 --- a/.github/scripts/test-dot-net.sh +++ b/.github/scripts/test-dot-net.sh @@ -2,7 +2,10 @@ cd dotnet-examples/ -cd spoken-language-identification +cd streaming-hlg-decoding/ +./run.sh + +cd ../spoken-language-identification ./run.sh cd ../online-decode-files diff --git a/.github/scripts/test-nodejs-npm.sh b/.github/scripts/test-nodejs-npm.sh index c205d3880..1531aff2c 100755 --- a/.github/scripts/test-nodejs-npm.sh +++ b/.github/scripts/test-nodejs-npm.sh @@ -58,6 +58,13 @@ rm sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13.tar.bz2 node ./test-online-zipformer2-ctc.js rm -rf sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13 + +curl -LS -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +tar xvf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +rm sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +node ./test-online-zipformer2-ctc-hlg.js +rm -rf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18 + # offline tts curl -LS -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-piper-en_US-amy-low.tar.bz2 diff --git a/.github/scripts/test-swift.sh b/.github/scripts/test-swift.sh index ec276c416..536c04c47 100755 --- a/.github/scripts/test-swift.sh +++ b/.github/scripts/test-swift.sh @@ -7,6 +7,10 @@ echo "pwd: $PWD" cd swift-api-examples ls -lh +./run-streaming-hlg-decode-file.sh +rm ./streaming-hlg-decode-file +rm -rf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18 + ./run-spoken-language-identification.sh rm -rf sherpa-onnx-whisper* @@ -31,4 +35,5 @@ sed -i.bak '20d' ./decode-file.swift ./run-decode-file-non-streaming.sh + ls -lh diff --git a/.github/workflows/test-dot-net.yaml b/.github/workflows/test-dot-net.yaml index aa8e7b1e3..243b4f1a5 100644 --- a/.github/workflows/test-dot-net.yaml +++ b/.github/workflows/test-dot-net.yaml @@ -178,6 +178,7 @@ jobs: cp -v scripts/dotnet/examples/online-decode-files.csproj dotnet-examples/online-decode-files/ cp -v scripts/dotnet/examples/speech-recognition-from-microphone.csproj dotnet-examples/speech-recognition-from-microphone/ cp -v scripts/dotnet/examples/spoken-language-identification.csproj dotnet-examples/spoken-language-identification/ + cp -v scripts/dotnet/examples/streaming-hlg-decoding.csproj dotnet-examples/streaming-hlg-decoding ls -lh /tmp diff --git a/.github/workflows/test-go-package.yaml b/.github/workflows/test-go-package.yaml index d761be4f8..271329500 100644 --- a/.github/workflows/test-go-package.yaml +++ b/.github/workflows/test-go-package.yaml @@ -66,12 +66,77 @@ jobs: run: | gcc --version - - name: Test speaker identification + - name: Test streaming HLG decoding (Linux/macOS) + if: matrix.os != 'windows-latest' + shell: bash + run: | + cd go-api-examples/streaming-hlg-decoding/ + ./run.sh + + - name: Test speaker identification (Linux/macOS) + if: matrix.os != 'windows-latest' shell: bash run: | cd go-api-examples/speaker-identification ./run.sh + - name: Test speaker identification (Win64) + if: matrix.os == 'windows-latest' && matrix.arch == 'x64' + shell: bash + run: | + cd go-api-examples/speaker-identification + go mod tidy + cat go.mod + go build + + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/3dspeaker_speech_campplus_sv_zh-cn_16k-common.onnx + git clone https://github.com/csukuangfj/sr-data + ls -lh + echo $PWD + ls -lh /C/Users/runneradmin/go/pkg/mod/github.com/k2-fsa/ + ls -lh /C/Users/runneradmin/go/pkg/mod/github.com/k2-fsa/* + cp -v /C/Users/runneradmin/go/pkg/mod/github.com/k2-fsa/sherpa-onnx-go-windows*/lib/x86_64-pc-windows-gnu/*.dll . + ls -lh + go mod tidy + go build + go run ./main.go + + - name: Test speaker identification (Win32) + if: matrix.os == 'windows-latest' && matrix.arch == 'x86' + shell: bash + run: | + cd go-api-examples/speaker-identification + go mod tidy + cat go.mod + ls -lh + + go env GOARCH + go env + echo "------------------------------" + go env -w GOARCH=386 + go env -w CGO_ENABLED=1 + go env + + go clean + go build + + echo $PWD + + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/3dspeaker_speech_campplus_sv_zh-cn_16k-common.onnx + git clone https://github.com/csukuangfj/sr-data + ls -lh + echo $PWD + ls -lh /C/Users/runneradmin/go/pkg/mod/github.com/k2-fsa/ + ls -lh /C/Users/runneradmin/go/pkg/mod/github.com/k2-fsa/* + cp -v /C/Users/runneradmin/go/pkg/mod/github.com/k2-fsa/sherpa-onnx-go-windows*/lib/i686-pc-windows-gnu/*.dll . + ls -lh + go mod tidy + go build + go run ./main.go + + rm -rf sr-data + rm -rf *.onnx + - name: Test non-streaming TTS (Linux/macOS) if: matrix.os != 'windows-latest' shell: bash diff --git a/.github/workflows/test-go.yaml b/.github/workflows/test-go.yaml index 298403ecf..17af77e62 100644 --- a/.github/workflows/test-go.yaml +++ b/.github/workflows/test-go.yaml @@ -74,6 +74,12 @@ jobs: go mod tidy go build + - name: Test streaming HLG decoding + shell: bash + run: | + cd scripts/go/_internal/streaming-hlg-decoding/ + ./run.sh + - name: Test speaker identification shell: bash run: | diff --git a/c-api-examples/CMakeLists.txt b/c-api-examples/CMakeLists.txt index 069563246..4c3669d1f 100644 --- a/c-api-examples/CMakeLists.txt +++ b/c-api-examples/CMakeLists.txt @@ -15,6 +15,9 @@ target_link_libraries(spoken-language-identification-c-api sherpa-onnx-c-api) add_executable(speaker-identification-c-api speaker-identification-c-api.c) target_link_libraries(speaker-identification-c-api sherpa-onnx-c-api) +add_executable(streaming-hlg-decode-file-c-api streaming-hlg-decode-file-c-api.c) +target_link_libraries(streaming-hlg-decode-file-c-api sherpa-onnx-c-api) + if(SHERPA_ONNX_HAS_ALSA) add_subdirectory(./asr-microphone-example) elseif((UNIX AND NOT APPLE) OR LINUX) diff --git a/c-api-examples/streaming-hlg-decode-file-c-api.c b/c-api-examples/streaming-hlg-decode-file-c-api.c new file mode 100644 index 000000000..83422def4 --- /dev/null +++ b/c-api-examples/streaming-hlg-decode-file-c-api.c @@ -0,0 +1,130 @@ +// c-api-examples/streaming-hlg-decode-file-c-api.c +// +// Copyright (c) 2024 Xiaomi Corporation +/* +We use the following model as an example + +// clang-format off + +Download the model from +https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 + +tar xvf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +rm sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 + +build/bin/streaming-hlg-decode-file-c-api + +(The above model is from https://github.com/k2-fsa/icefall/pull/1557) +*/ +#include +#include +#include + +#include "sherpa-onnx/c-api/c-api.h" + +int32_t main() { + // clang-format off + // + // Please download the model from + // https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 + const char *model = "./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/ctc-epoch-30-avg-3-chunk-16-left-128.int8.onnx"; + const char *tokens = "./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/tokens.txt"; + const char *graph = "./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/HLG.fst"; + const char *wav_filename = "./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/test_wavs/8k.wav"; + // clang-format on + + SherpaOnnxOnlineRecognizerConfig config; + + memset(&config, 0, sizeof(config)); + config.feat_config.sample_rate = 16000; + config.feat_config.feature_dim = 80; + config.model_config.zipformer2_ctc.model = model; + config.model_config.tokens = tokens; + config.model_config.num_threads = 1; + config.model_config.provider = "cpu"; + config.model_config.debug = 0; + config.ctc_fst_decoder_config.graph = graph; + const SherpaOnnxOnlineRecognizer *recognizer = + CreateOnlineRecognizer(&config); + if (!recognizer) { + fprintf(stderr, "Failed to create recognizer"); + exit(-1); + } + + const SherpaOnnxOnlineStream *stream = CreateOnlineStream(recognizer); + + const SherpaOnnxDisplay *display = CreateDisplay(50); + int32_t segment_id = 0; + + const SherpaOnnxWave *wave = SherpaOnnxReadWave(wav_filename); + if (wave == NULL) { + fprintf(stderr, "Failed to read %s\n", wav_filename); + exit(-1); + } + +// simulate streaming. You can choose an arbitrary N +#define N 3200 + + int16_t buffer[N]; + float samples[N]; + fprintf(stderr, "sample rate: %d, num samples: %d, duration: %.2f s\n", + wave->sample_rate, wave->num_samples, + (float)wave->num_samples / wave->sample_rate); + + int32_t k = 0; + while (k < wave->num_samples) { + int32_t start = k; + int32_t end = + (start + N > wave->num_samples) ? wave->num_samples : (start + N); + k += N; + + AcceptWaveform(stream, wave->sample_rate, wave->samples + start, + end - start); + while (IsOnlineStreamReady(recognizer, stream)) { + DecodeOnlineStream(recognizer, stream); + } + + const SherpaOnnxOnlineRecognizerResult *r = + GetOnlineStreamResult(recognizer, stream); + + if (strlen(r->text)) { + SherpaOnnxPrint(display, segment_id, r->text); + } + + if (IsEndpoint(recognizer, stream)) { + if (strlen(r->text)) { + ++segment_id; + } + Reset(recognizer, stream); + } + + DestroyOnlineRecognizerResult(r); + } + + // add some tail padding + float tail_paddings[4800] = {0}; // 0.3 seconds at 16 kHz sample rate + AcceptWaveform(stream, wave->sample_rate, tail_paddings, 4800); + + SherpaOnnxFreeWave(wave); + + InputFinished(stream); + while (IsOnlineStreamReady(recognizer, stream)) { + DecodeOnlineStream(recognizer, stream); + } + + const SherpaOnnxOnlineRecognizerResult *r = + GetOnlineStreamResult(recognizer, stream); + + if (strlen(r->text)) { + SherpaOnnxPrint(display, segment_id, r->text); + } + + DestroyOnlineRecognizerResult(r); + + DestroyDisplay(display); + DestroyOnlineStream(stream); + DestroyOnlineRecognizer(recognizer); + fprintf(stderr, "\n"); + + return 0; +} diff --git a/cmake/onnxruntime.cmake b/cmake/onnxruntime.cmake index fe2992ed0..ae22bfab0 100644 --- a/cmake/onnxruntime.cmake +++ b/cmake/onnxruntime.cmake @@ -5,7 +5,7 @@ function(download_onnxruntime) message(STATUS "CMAKE_SYSTEM_NAME: ${CMAKE_SYSTEM_NAME}") message(STATUS "CMAKE_SYSTEM_PROCESSOR: ${CMAKE_SYSTEM_PROCESSOR}") if(SHERPA_ONNX_ENABLE_WASM) - include(onnxruntime-wasm-simd) + include(onnxruntime-wasm-simd) elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL riscv64) if(BUILD_SHARED_LIBS) include(onnxruntime-linux-riscv64) diff --git a/dotnet-examples/sherpa-onnx.sln b/dotnet-examples/sherpa-onnx.sln index 6c469ba38..ff514df37 100644 --- a/dotnet-examples/sherpa-onnx.sln +++ b/dotnet-examples/sherpa-onnx.sln @@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "offline-tts-play", "offline EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spoken-language-identification", "spoken-language-identification\spoken-language-identification.csproj", "{3D7CF3D6-AC45-4D50-9619-5687B1443E94}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "streaming-hlg-decoding", "streaming-hlg-decoding\streaming-hlg-decoding.csproj", "{C4A368A5-FCA0-419D-97C9-C8CE0B08EB99}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -48,5 +50,9 @@ Global {3D7CF3D6-AC45-4D50-9619-5687B1443E94}.Debug|Any CPU.Build.0 = Debug|Any CPU {3D7CF3D6-AC45-4D50-9619-5687B1443E94}.Release|Any CPU.ActiveCfg = Release|Any CPU {3D7CF3D6-AC45-4D50-9619-5687B1443E94}.Release|Any CPU.Build.0 = Release|Any CPU + {C4A368A5-FCA0-419D-97C9-C8CE0B08EB99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4A368A5-FCA0-419D-97C9-C8CE0B08EB99}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4A368A5-FCA0-419D-97C9-C8CE0B08EB99}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4A368A5-FCA0-419D-97C9-C8CE0B08EB99}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/dotnet-examples/streaming-hlg-decoding/Program.cs b/dotnet-examples/streaming-hlg-decoding/Program.cs new file mode 100644 index 000000000..6ac7c8c94 --- /dev/null +++ b/dotnet-examples/streaming-hlg-decoding/Program.cs @@ -0,0 +1,66 @@ +// Copyright (c) 2024 Xiaomi Corporation +// +// This file shows how to do streaming HLG decoding. +// +// 1. Download the model for testing +// +// curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +// tar xvf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +// rm sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +// +// 2. Now run it +// +// dotnet run + +using SherpaOnnx; +using System.Collections.Generic; +using System; + +class StreamingHlgDecodingDemo +{ + + static void Main(string[] args) + { + var config = new OnlineRecognizerConfig(); + config.FeatConfig.SampleRate = 16000; + config.FeatConfig.FeatureDim = 80; + config.ModelConfig.Zipformer2Ctc.Model = "./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/ctc-epoch-30-avg-3-chunk-16-left-128.int8.onnx"; + + config.ModelConfig.Tokens = "./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/tokens.txt"; + config.ModelConfig.Provider = "cpu"; + config.ModelConfig.NumThreads = 1; + config.ModelConfig.Debug = 0; + config.CtcFstDecoderConfig.Graph = "./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/HLG.fst"; + + OnlineRecognizer recognizer = new OnlineRecognizer(config); + + var filename = "./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/test_wavs/8k.wav"; + + WaveReader waveReader = new WaveReader(filename); + OnlineStream s = recognizer.CreateStream(); + s.AcceptWaveform(waveReader.SampleRate, waveReader.Samples); + + float[] tailPadding = new float[(int)(waveReader.SampleRate * 0.3)]; + s.AcceptWaveform(waveReader.SampleRate, tailPadding); + s.InputFinished(); + + while (recognizer.IsReady(s)) + { + recognizer.Decode(s); + } + + OnlineRecognizerResult r = recognizer.GetResult(s); + var text = r.Text; + var tokens = r.Tokens; + Console.WriteLine("--------------------"); + Console.WriteLine(filename); + Console.WriteLine("text: {0}", text); + Console.WriteLine("tokens: [{0}]", string.Join(", ", tokens)); + Console.Write("timestamps: ["); + r.Timestamps.ToList().ForEach(i => Console.Write(String.Format("{0:0.00}", i) + ", ")); + Console.WriteLine("]"); + Console.WriteLine("--------------------"); + } +} + + diff --git a/dotnet-examples/streaming-hlg-decoding/WaveReader.cs b/dotnet-examples/streaming-hlg-decoding/WaveReader.cs new file mode 120000 index 000000000..bedfc6343 --- /dev/null +++ b/dotnet-examples/streaming-hlg-decoding/WaveReader.cs @@ -0,0 +1 @@ +../online-decode-files/WaveReader.cs \ No newline at end of file diff --git a/dotnet-examples/streaming-hlg-decoding/run.sh b/dotnet-examples/streaming-hlg-decoding/run.sh new file mode 100755 index 000000000..2e0319740 --- /dev/null +++ b/dotnet-examples/streaming-hlg-decoding/run.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -ex + +if [ ! -f ./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/HLG.fst ]; then + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 + tar xvf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 + rm sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +fi + +dotnet run -c Release diff --git a/dotnet-examples/streaming-hlg-decoding/streaming-hlg-decoding.csproj b/dotnet-examples/streaming-hlg-decoding/streaming-hlg-decoding.csproj new file mode 100644 index 000000000..6030ec851 --- /dev/null +++ b/dotnet-examples/streaming-hlg-decoding/streaming-hlg-decoding.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + streaming_hlg_decoding + enable + enable + + + + + + + diff --git a/go-api-examples/streaming-hlg-decoding/go.mod b/go-api-examples/streaming-hlg-decoding/go.mod new file mode 100644 index 000000000..1b9b98930 --- /dev/null +++ b/go-api-examples/streaming-hlg-decoding/go.mod @@ -0,0 +1,3 @@ +module streaming-hlg-decoding + +go 1.12 diff --git a/go-api-examples/streaming-hlg-decoding/main.go b/go-api-examples/streaming-hlg-decoding/main.go new file mode 100644 index 000000000..8c0a9700f --- /dev/null +++ b/go-api-examples/streaming-hlg-decoding/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "bytes" + "encoding/binary" + sherpa "github.com/k2-fsa/sherpa-onnx-go/sherpa_onnx" + "github.com/youpy/go-wav" + "log" + "os" + "strings" +) + +func main() { + log.SetFlags(log.LstdFlags | log.Lmicroseconds) + + config := sherpa.OnlineRecognizerConfig{} + config.FeatConfig = sherpa.FeatureConfig{SampleRate: 16000, FeatureDim: 80} + + // please download model files from + // https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 + config.ModelConfig.Zipformer2Ctc.Model = "./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/ctc-epoch-30-avg-3-chunk-16-left-128.int8.onnx" + config.ModelConfig.Tokens = "./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/tokens.txt" + + config.ModelConfig.NumThreads = 1 + config.ModelConfig.Debug = 0 + config.ModelConfig.Provider = "cpu" + config.CtcFstDecoderConfig.Graph = "./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/HLG.fst" + + wav_filename := "./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/test_wavs/8k.wav" + + samples, sampleRate := readWave(wav_filename) + + log.Println("Initializing recognizer (may take several seconds)") + recognizer := sherpa.NewOnlineRecognizer(&config) + log.Println("Recognizer created!") + defer sherpa.DeleteOnlineRecognizer(recognizer) + + log.Println("Start decoding!") + stream := sherpa.NewOnlineStream(recognizer) + defer sherpa.DeleteOnlineStream(stream) + + stream.AcceptWaveform(sampleRate, samples) + + tailPadding := make([]float32, int(float32(sampleRate)*0.3)) + stream.AcceptWaveform(sampleRate, tailPadding) + + for recognizer.IsReady(stream) { + recognizer.Decode(stream) + } + log.Println("Decoding done!") + result := recognizer.GetResult(stream) + log.Println(strings.ToLower(result.Text)) + log.Printf("Wave duration: %v seconds", float32(len(samples))/float32(sampleRate)) +} + +func readWave(filename string) (samples []float32, sampleRate int) { + file, _ := os.Open(filename) + defer file.Close() + + reader := wav.NewReader(file) + format, err := reader.Format() + if err != nil { + log.Fatalf("Failed to read wave format") + } + + if format.AudioFormat != 1 { + log.Fatalf("Support only PCM format. Given: %v\n", format.AudioFormat) + } + + if format.NumChannels != 1 { + log.Fatalf("Support only 1 channel wave file. Given: %v\n", format.NumChannels) + } + + if format.BitsPerSample != 16 { + log.Fatalf("Support only 16-bit per sample. Given: %v\n", format.BitsPerSample) + } + + reader.Duration() // so that it initializes reader.Size + + buf := make([]byte, reader.Size) + n, err := reader.Read(buf) + if n != int(reader.Size) { + log.Fatalf("Failed to read %v bytes. Returned %v bytes\n", reader.Size, n) + } + + samples = samplesInt16ToFloat(buf) + sampleRate = int(format.SampleRate) + + return +} + +func samplesInt16ToFloat(inSamples []byte) []float32 { + numSamples := len(inSamples) / 2 + outSamples := make([]float32, numSamples) + + for i := 0; i != numSamples; i++ { + s := inSamples[i*2 : (i+1)*2] + + var s16 int16 + buf := bytes.NewReader(s) + err := binary.Read(buf, binary.LittleEndian, &s16) + if err != nil { + log.Fatal("Failed to parse 16-bit sample") + } + outSamples[i] = float32(s16) / 32768 + } + + return outSamples +} diff --git a/go-api-examples/streaming-hlg-decoding/run.sh b/go-api-examples/streaming-hlg-decoding/run.sh new file mode 100755 index 000000000..fb7549c5e --- /dev/null +++ b/go-api-examples/streaming-hlg-decoding/run.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -ex + +if [ ! -f ./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/HLG.fst ]; then + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 + tar xvf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 + rm sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +fi + +go mod tidy +go build +ls -lh +./streaming-hlg-decoding diff --git a/nodejs-examples/README.md b/nodejs-examples/README.md index f2dc14c92..9c13bee51 100644 --- a/nodejs-examples/README.md +++ b/nodejs-examples/README.md @@ -174,3 +174,16 @@ wget -q https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherp tar xvf sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13.tar.bz2 node ./test-online-zipformer2-ctc.js ``` + +## ./test-online-zipformer2-ctc-hlg.js +[./test-online-zipformer2-ctc-hlg.js](./test-online-zipformer2-ctc-hlg.js) demonstrates +how to decode a file using a streaming zipformer2 CTC model with HLG. In the code +we use [sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18](https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2). + +You can use the following command to run it: + +```bash +wget https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +tar xvf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +node ./test-online-zipformer2-ctc-hlg.js +``` diff --git a/nodejs-examples/test-online-paraformer-microphone.js b/nodejs-examples/test-online-paraformer-microphone.js index 4b76f4cd1..591b4dbb5 100644 --- a/nodejs-examples/test-online-paraformer-microphone.js +++ b/nodejs-examples/test-online-paraformer-microphone.js @@ -50,6 +50,10 @@ function createOnlineRecognizer() { rule3MinUtteranceLength: 20, hotwordsFile: '', hotwordsScore: 1.5, + ctcFstDecoderConfig: { + graph: '', + maxActive: 3000, + } }; return sherpa_onnx.createOnlineRecognizer(recognizerConfig); diff --git a/nodejs-examples/test-online-paraformer.js b/nodejs-examples/test-online-paraformer.js index 09982988e..01b8feeb9 100644 --- a/nodejs-examples/test-online-paraformer.js +++ b/nodejs-examples/test-online-paraformer.js @@ -51,6 +51,10 @@ function createOnlineRecognizer() { rule3MinUtteranceLength: 20, hotwordsFile: '', hotwordsScore: 1.5, + ctcFstDecoderConfig: { + graph: '', + maxActive: 3000, + } }; return sherpa_onnx.createOnlineRecognizer(recognizerConfig); diff --git a/nodejs-examples/test-online-transducer-microphone.js b/nodejs-examples/test-online-transducer-microphone.js index 9fa7c92c4..6312b5679 100644 --- a/nodejs-examples/test-online-transducer-microphone.js +++ b/nodejs-examples/test-online-transducer-microphone.js @@ -52,6 +52,10 @@ function createOnlineRecognizer() { rule3MinUtteranceLength: 20, hotwordsFile: '', hotwordsScore: 1.5, + ctcFstDecoderConfig: { + graph: '', + maxActive: 3000, + } }; return sherpa_onnx.createOnlineRecognizer(recognizerConfig); diff --git a/nodejs-examples/test-online-transducer.js b/nodejs-examples/test-online-transducer.js index 4293cbc9b..e4bb46d2e 100644 --- a/nodejs-examples/test-online-transducer.js +++ b/nodejs-examples/test-online-transducer.js @@ -53,6 +53,10 @@ function createOnlineRecognizer() { rule3MinUtteranceLength: 20, hotwordsFile: '', hotwordsScore: 1.5, + ctcFstDecoderConfig: { + graph: '', + maxActive: 3000, + } }; return sherpa_onnx.createOnlineRecognizer(recognizerConfig); diff --git a/nodejs-examples/test-online-zipformer2-ctc-hlg.js b/nodejs-examples/test-online-zipformer2-ctc-hlg.js new file mode 100644 index 000000000..1bf999927 --- /dev/null +++ b/nodejs-examples/test-online-zipformer2-ctc-hlg.js @@ -0,0 +1,125 @@ +// Copyright (c) 2023 Xiaomi Corporation (authors: Fangjun Kuang) +// +const fs = require('fs'); +const {Readable} = require('stream'); +const wav = require('wav'); + +const sherpa_onnx = require('sherpa-onnx'); + +function createOnlineRecognizer() { + let onlineTransducerModelConfig = { + encoder: '', + decoder: '', + joiner: '', + }; + + let onlineParaformerModelConfig = { + encoder: '', + decoder: '', + }; + + let onlineZipformer2CtcModelConfig = { + model: + './sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/ctc-epoch-30-avg-3-chunk-16-left-128.int8.onnx', + }; + + let onlineModelConfig = { + transducer: onlineTransducerModelConfig, + paraformer: onlineParaformerModelConfig, + zipformer2Ctc: onlineZipformer2CtcModelConfig, + tokens: './sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/tokens.txt', + numThreads: 1, + provider: 'cpu', + debug: 0, + modelType: '', + }; + + let featureConfig = { + sampleRate: 16000, + featureDim: 80, + }; + + let recognizerConfig = { + featConfig: featureConfig, + modelConfig: onlineModelConfig, + decodingMethod: 'greedy_search', + maxActivePaths: 4, + enableEndpoint: 1, + rule1MinTrailingSilence: 2.4, + rule2MinTrailingSilence: 1.2, + rule3MinUtteranceLength: 20, + hotwordsFile: '', + hotwordsScore: 1.5, + ctcFstDecoderConfig: { + graph: './sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/HLG.fst', + maxActive: 3000, + } + }; + + return sherpa_onnx.createOnlineRecognizer(recognizerConfig); +} + +const recognizer = createOnlineRecognizer(); +const stream = recognizer.createStream(); + +const waveFilename = + './sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/test_wavs/8k.wav'; + +const reader = new wav.Reader(); +const readable = new Readable().wrap(reader); + +function decode(samples) { + stream.acceptWaveform(gSampleRate, samples); + + while (recognizer.isReady(stream)) { + recognizer.decode(stream); + } + const text = recognizer.getResult(stream); + console.log(text); +} + +let gSampleRate = 16000; + +reader.on('format', ({audioFormat, bitDepth, channels, sampleRate}) => { + gSampleRate = sampleRate; + + if (audioFormat != 1) { + throw new Error(`Only support PCM format. Given ${audioFormat}`); + } + + if (channels != 1) { + throw new Error(`Only a single channel. Given ${channel}`); + } + + if (bitDepth != 16) { + throw new Error(`Only support 16-bit samples. Given ${bitDepth}`); + } +}); + +fs.createReadStream(waveFilename, {'highWaterMark': 4096}) + .pipe(reader) + .on('finish', function(err) { + // tail padding + const floatSamples = + new Float32Array(recognizer.config.featConfig.sampleRate * 0.5); + decode(floatSamples); + stream.free(); + recognizer.free(); + }); + +readable.on('readable', function() { + let chunk; + while ((chunk = readable.read()) != null) { + const int16Samples = new Int16Array( + chunk.buffer, chunk.byteOffset, + chunk.length / Int16Array.BYTES_PER_ELEMENT); + + const floatSamples = new Float32Array(int16Samples.length); + + for (let i = 0; i < floatSamples.length; i++) { + floatSamples[i] = int16Samples[i] / 32768.0; + } + + decode(floatSamples); + } +}); diff --git a/nodejs-examples/test-online-zipformer2-ctc.js b/nodejs-examples/test-online-zipformer2-ctc.js index 4f3506a28..2e85d69a0 100644 --- a/nodejs-examples/test-online-zipformer2-ctc.js +++ b/nodejs-examples/test-online-zipformer2-ctc.js @@ -51,6 +51,10 @@ function createOnlineRecognizer() { rule3MinUtteranceLength: 20, hotwordsFile: '', hotwordsScore: 1.5, + ctcFstDecoderConfig: { + graph: '', + maxActive: 3000, + } }; return sherpa_onnx.createOnlineRecognizer(recognizerConfig); diff --git a/scripts/dotnet/examples/streaming-hlg-decoding.csproj b/scripts/dotnet/examples/streaming-hlg-decoding.csproj new file mode 100644 index 000000000..4b982c312 --- /dev/null +++ b/scripts/dotnet/examples/streaming-hlg-decoding.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + streaming_hlg_decoding + enable + enable + + + + /tmp/packages;$(RestoreSources);https://api.nuget.org/v3/index.json + + + + + + + diff --git a/scripts/dotnet/online.cs b/scripts/dotnet/online.cs index 09b827ad1..a9dd95dec 100644 --- a/scripts/dotnet/online.cs +++ b/scripts/dotnet/online.cs @@ -116,6 +116,21 @@ public FeatureConfig() public int FeatureDim; } + [StructLayout(LayoutKind.Sequential)] + public struct OnlineCtcFstDecoderConfig + { + public OnlineCtcFstDecoderConfig() + { + Graph = ""; + MaxActive = 3000; + } + + [MarshalAs(UnmanagedType.LPStr)] + public string Graph; + + public int MaxActive; + } + [StructLayout(LayoutKind.Sequential)] public struct OnlineRecognizerConfig { @@ -131,6 +146,7 @@ public OnlineRecognizerConfig() Rule3MinUtteranceLength = 20.0F; HotwordsFile = ""; HotwordsScore = 1.5F; + CtcFstDecoderConfig = new OnlineCtcFstDecoderConfig(); } public FeatureConfig FeatConfig; public OnlineModelConfig ModelConfig; @@ -167,6 +183,8 @@ public OnlineRecognizerConfig() /// Bonus score for each token in hotwords. public float HotwordsScore; + + public OnlineCtcFstDecoderConfig CtcFstDecoderConfig; } public class OnlineRecognizerResult diff --git a/scripts/go/_internal/streaming-hlg-decoding/.gitignore b/scripts/go/_internal/streaming-hlg-decoding/.gitignore new file mode 100644 index 000000000..4bc5d691e --- /dev/null +++ b/scripts/go/_internal/streaming-hlg-decoding/.gitignore @@ -0,0 +1 @@ +streaming-hlg-decoding diff --git a/scripts/go/_internal/streaming-hlg-decoding/go.mod b/scripts/go/_internal/streaming-hlg-decoding/go.mod new file mode 100644 index 000000000..55c0c92a6 --- /dev/null +++ b/scripts/go/_internal/streaming-hlg-decoding/go.mod @@ -0,0 +1,5 @@ +module streaming-hlg-decoding + +go 1.12 + +replace github.com/k2-fsa/sherpa-onnx-go/sherpa_onnx => ../ diff --git a/scripts/go/_internal/streaming-hlg-decoding/main.go b/scripts/go/_internal/streaming-hlg-decoding/main.go new file mode 120000 index 000000000..0b7bc3b96 --- /dev/null +++ b/scripts/go/_internal/streaming-hlg-decoding/main.go @@ -0,0 +1 @@ +../../../../go-api-examples/streaming-hlg-decoding/main.go \ No newline at end of file diff --git a/scripts/go/_internal/streaming-hlg-decoding/run.sh b/scripts/go/_internal/streaming-hlg-decoding/run.sh new file mode 120000 index 000000000..894404716 --- /dev/null +++ b/scripts/go/_internal/streaming-hlg-decoding/run.sh @@ -0,0 +1 @@ +../../../../go-api-examples/streaming-hlg-decoding/run.sh \ No newline at end of file diff --git a/scripts/go/sherpa_onnx.go b/scripts/go/sherpa_onnx.go index 1b4c60abe..361d9775f 100644 --- a/scripts/go/sherpa_onnx.go +++ b/scripts/go/sherpa_onnx.go @@ -99,6 +99,11 @@ type FeatureConfig struct { FeatureDim int } +type OnlineCtcFstDecoderConfig struct { + Graph string + MaxActive int +} + // Configuration for the online/streaming recognizer. type OnlineRecognizerConfig struct { FeatConfig FeatureConfig @@ -120,6 +125,7 @@ type OnlineRecognizerConfig struct { Rule1MinTrailingSilence float32 Rule2MinTrailingSilence float32 Rule3MinUtteranceLength float32 + CtcFstDecoderConfig OnlineCtcFstDecoderConfig } // It contains the recognition result for a online stream. @@ -190,6 +196,10 @@ func NewOnlineRecognizer(config *OnlineRecognizerConfig) *OnlineRecognizer { c.rule2_min_trailing_silence = C.float(config.Rule2MinTrailingSilence) c.rule3_min_utterance_length = C.float(config.Rule3MinUtteranceLength) + c.ctc_fst_decoder_config.graph = C.CString(config.CtcFstDecoderConfig.Graph) + defer C.free(unsafe.Pointer(c.ctc_fst_decoder_config.graph)) + c.ctc_fst_decoder_config.max_active = C.int(config.CtcFstDecoderConfig.MaxActive) + recognizer := &OnlineRecognizer{} recognizer.impl = C.CreateOnlineRecognizer(&c) diff --git a/sherpa-onnx/c-api/c-api.cc b/sherpa-onnx/c-api/c-api.cc index 685091c1e..8baecd06b 100644 --- a/sherpa-onnx/c-api/c-api.cc +++ b/sherpa-onnx/c-api/c-api.cc @@ -99,6 +99,11 @@ SherpaOnnxOnlineRecognizer *CreateOnlineRecognizer( recognizer_config.hotwords_score = SHERPA_ONNX_OR(config->hotwords_score, 1.5); + recognizer_config.ctc_fst_decoder_config.graph = + SHERPA_ONNX_OR(config->ctc_fst_decoder_config.graph, ""); + recognizer_config.ctc_fst_decoder_config.max_active = + SHERPA_ONNX_OR(config->ctc_fst_decoder_config.max_active, 3000); + if (config->model_config.debug) { SHERPA_ONNX_LOGE("%s\n", recognizer_config.ToString().c_str()); } diff --git a/sherpa-onnx/c-api/c-api.h b/sherpa-onnx/c-api/c-api.h index 66c33bf21..55ad46632 100644 --- a/sherpa-onnx/c-api/c-api.h +++ b/sherpa-onnx/c-api/c-api.h @@ -96,6 +96,11 @@ SHERPA_ONNX_API typedef struct SherpaOnnxFeatureConfig { int32_t feature_dim; } SherpaOnnxFeatureConfig; +SHERPA_ONNX_API typedef struct SherpaOnnxOnlineCtcFstDecoderConfig { + const char *graph; + int32_t max_active; +} SherpaOnnxOnlineCtcFstDecoderConfig; + SHERPA_ONNX_API typedef struct SherpaOnnxOnlineRecognizerConfig { SherpaOnnxFeatureConfig feat_config; SherpaOnnxOnlineModelConfig model_config; @@ -131,6 +136,8 @@ SHERPA_ONNX_API typedef struct SherpaOnnxOnlineRecognizerConfig { /// Bonus score for each token in hotwords. float hotwords_score; + + SherpaOnnxOnlineCtcFstDecoderConfig ctc_fst_decoder_config; } SherpaOnnxOnlineRecognizerConfig; SHERPA_ONNX_API typedef struct SherpaOnnxOnlineRecognizerResult { diff --git a/swift-api-examples/.gitignore b/swift-api-examples/.gitignore index 4b76201d0..f4290242b 100644 --- a/swift-api-examples/.gitignore +++ b/swift-api-examples/.gitignore @@ -7,3 +7,4 @@ vits-vctk sherpa-onnx-paraformer-zh-2023-09-14 !*.sh *.bak +streaming-hlg-decode-file diff --git a/swift-api-examples/SherpaOnnx.swift b/swift-api-examples/SherpaOnnx.swift index c93fbf37b..b463c8667 100644 --- a/swift-api-examples/SherpaOnnx.swift +++ b/swift-api-examples/SherpaOnnx.swift @@ -111,6 +111,15 @@ func sherpaOnnxFeatureConfig( feature_dim: Int32(featureDim)) } +func sherpaOnnxOnlineCtcFstDecoderConfig( + graph: String = "", + maxActive: Int = 3000 +) -> SherpaOnnxOnlineCtcFstDecoderConfig { + return SherpaOnnxOnlineCtcFstDecoderConfig( + graph: toCPointer(graph), + max_active: Int32(maxActive)) +} + func sherpaOnnxOnlineRecognizerConfig( featConfig: SherpaOnnxFeatureConfig, modelConfig: SherpaOnnxOnlineModelConfig, @@ -121,7 +130,8 @@ func sherpaOnnxOnlineRecognizerConfig( decodingMethod: String = "greedy_search", maxActivePaths: Int = 4, hotwordsFile: String = "", - hotwordsScore: Float = 1.5 + hotwordsScore: Float = 1.5, + ctcFstDecoderConfig: SherpaOnnxOnlineCtcFstDecoderConfig = sherpaOnnxOnlineCtcFstDecoderConfig() ) -> SherpaOnnxOnlineRecognizerConfig { return SherpaOnnxOnlineRecognizerConfig( feat_config: featConfig, @@ -133,7 +143,9 @@ func sherpaOnnxOnlineRecognizerConfig( rule2_min_trailing_silence: rule2MinTrailingSilence, rule3_min_utterance_length: rule3MinUtteranceLength, hotwords_file: toCPointer(hotwordsFile), - hotwords_score: hotwordsScore) + hotwords_score: hotwordsScore, + ctc_fst_decoder_config: ctcFstDecoderConfig + ) } /// Wrapper for recognition result. diff --git a/swift-api-examples/run-streaming-hlg-decode-file.sh b/swift-api-examples/run-streaming-hlg-decode-file.sh new file mode 100755 index 000000000..5b641b8b9 --- /dev/null +++ b/swift-api-examples/run-streaming-hlg-decode-file.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -ex + +if [ ! -d ../build-swift-macos ]; then + echo "Please run ../build-swift-macos.sh first!" + exit 1 +fi + +if [ ! -f ./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/HLG.fst ]; then + echo "Downloading the pre-trained model for testing." + + wget -q https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 + tar xvf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 + rm sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +fi + +if [ ! -e ./streaming-hlg-decode-file ]; then + # Note: We use -lc++ to link against libc++ instead of libstdc++ + swiftc \ + -lc++ \ + -I ../build-swift-macos/install/include \ + -import-objc-header ./SherpaOnnx-Bridging-Header.h \ + ./streaming-hlg-decode-file.swift ./SherpaOnnx.swift \ + -L ../build-swift-macos/install/lib/ \ + -l sherpa-onnx \ + -l onnxruntime \ + -o streaming-hlg-decode-file + + strip ./streaming-hlg-decode-file +else + echo "./streaming-hlg-decode-file exists - skip building" +fi + +export DYLD_LIBRARY_PATH=$PWD/../build-swift-macos/install/lib:$DYLD_LIBRARY_PATH +./streaming-hlg-decode-file diff --git a/swift-api-examples/streaming-hlg-decode-file.swift b/swift-api-examples/streaming-hlg-decode-file.swift new file mode 100644 index 000000000..e57d118ea --- /dev/null +++ b/swift-api-examples/streaming-hlg-decode-file.swift @@ -0,0 +1,79 @@ +import AVFoundation + +extension AudioBuffer { + func array() -> [Float] { + return Array(UnsafeBufferPointer(self)) + } +} + +extension AVAudioPCMBuffer { + func array() -> [Float] { + return self.audioBufferList.pointee.mBuffers.array() + } +} + +func run() { + let filePath = + "./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/test_wavs/8k.wav" + let model = + "./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/ctc-epoch-30-avg-3-chunk-16-left-128.int8.onnx" + let tokens = "./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/tokens.txt" + let zipfomer2CtcModelConfig = sherpaOnnxOnlineZipformer2CtcModelConfig( + model: model + ) + + let modelConfig = sherpaOnnxOnlineModelConfig( + tokens: tokens, + zipformer2Ctc: zipfomer2CtcModelConfig + ) + + let featConfig = sherpaOnnxFeatureConfig( + sampleRate: 16000, + featureDim: 80 + ) + + let ctcFstDecoderConfig = sherpaOnnxOnlineCtcFstDecoderConfig( + graph: "./sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18/HLG.fst", + maxActive: 3000 + ) + + var config = sherpaOnnxOnlineRecognizerConfig( + featConfig: featConfig, + modelConfig: modelConfig, + ctcFstDecoderConfig: ctcFstDecoderConfig + ) + + let recognizer = SherpaOnnxRecognizer(config: &config) + + let fileURL: NSURL = NSURL(fileURLWithPath: filePath) + let audioFile = try! AVAudioFile(forReading: fileURL as URL) + + let audioFormat = audioFile.processingFormat + assert(audioFormat.channelCount == 1) + assert(audioFormat.commonFormat == AVAudioCommonFormat.pcmFormatFloat32) + + let audioFrameCount = UInt32(audioFile.length) + let audioFileBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: audioFrameCount) + + try! audioFile.read(into: audioFileBuffer!) + let array: [Float]! = audioFileBuffer?.array() + recognizer.acceptWaveform(samples: array, sampleRate: Int(audioFormat.sampleRate)) + + let tailPadding = [Float](repeating: 0.0, count: 3200) + recognizer.acceptWaveform(samples: tailPadding, sampleRate: Int(audioFormat.sampleRate)) + + recognizer.inputFinished() + while recognizer.isReady() { + recognizer.decode() + } + + let result = recognizer.getResult() + print("\nresult is:\n\(result.text)") +} + +@main +struct App { + static func main() { + run() + } +} diff --git a/wasm/asr/sherpa-onnx-asr.js b/wasm/asr/sherpa-onnx-asr.js index 55c8f2d9d..e61757cf9 100644 --- a/wasm/asr/sherpa-onnx-asr.js +++ b/wasm/asr/sherpa-onnx-asr.js @@ -43,6 +43,10 @@ function freeConfig(config, Module) { freeConfig(config.lm, Module) } + if ('ctcFstDecoder' in config) { + freeConfig(config.ctcFstDecoder, Module) + } + Module._free(config.ptr); } @@ -193,11 +197,26 @@ function initSherpaOnnxFeatureConfig(config, Module) { return {ptr: ptr, len: len}; } +function initSherpaOnnxOnlineCtcFstDecoderConfig(config, Module) { + const len = 2 * 4; + const ptr = Module._malloc(len); + + const graphLen = Module.lengthBytesUTF8(config.graph) + 1; + const buffer = Module._malloc(graphLen); + Module.stringToUTF8(config.graph, buffer, graphLen); + + Module.setValue(ptr, buffer, 'i8*'); + Module.setValue(ptr + 4, config.maxActive, 'i32'); + return {ptr: ptr, len: len, buffer: buffer}; +} + function initSherpaOnnxOnlineRecognizerConfig(config, Module) { const feat = initSherpaOnnxFeatureConfig(config.featConfig, Module); const model = initSherpaOnnxOnlineModelConfig(config.modelConfig, Module); + const ctcFstDecoder = initSherpaOnnxOnlineCtcFstDecoderConfig( + config.ctcFstDecoderConfig, Module) - const len = feat.len + model.len + 8 * 4; + const len = feat.len + model.len + 8 * 4 + ctcFstDecoder.len; const ptr = Module._malloc(len); let offset = 0; @@ -243,8 +262,11 @@ function initSherpaOnnxOnlineRecognizerConfig(config, Module) { Module.setValue(ptr + offset, config.hotwordsScore, 'float'); offset += 4; + Module._CopyHeap(ctcFstDecoder.ptr, ctcFstDecoder.len, ptr + offset); + return { - buffer: buffer, ptr: ptr, len: len, feat: feat, model: model + buffer: buffer, ptr: ptr, len: len, feat: feat, model: model, + ctcFstDecoder: ctcFstDecoder } } @@ -313,6 +335,10 @@ function createOnlineRecognizer(Module, myConfig) { rule3MinUtteranceLength: 20, hotwordsFile: '', hotwordsScore: 1.5, + ctcFstDecoderConfig: { + graph: '', + maxActive: 3000, + } }; if (myConfig) { recognizerConfig = myConfig; diff --git a/wasm/asr/sherpa-onnx-wasm-main-asr.cc b/wasm/asr/sherpa-onnx-wasm-main-asr.cc index 951391e14..70d13f1c4 100644 --- a/wasm/asr/sherpa-onnx-wasm-main-asr.cc +++ b/wasm/asr/sherpa-onnx-wasm-main-asr.cc @@ -22,9 +22,11 @@ static_assert(sizeof(SherpaOnnxOnlineModelConfig) == sizeof(SherpaOnnxOnlineZipformer2CtcModelConfig) + 5 * 4, ""); static_assert(sizeof(SherpaOnnxFeatureConfig) == 2 * 4, ""); +static_assert(sizeof(SherpaOnnxOnlineCtcFstDecoderConfig) == 2 * 4, ""); static_assert(sizeof(SherpaOnnxOnlineRecognizerConfig) == sizeof(SherpaOnnxFeatureConfig) + - sizeof(SherpaOnnxOnlineModelConfig) + 8 * 4, + sizeof(SherpaOnnxOnlineModelConfig) + 8 * 4 + + sizeof(SherpaOnnxOnlineCtcFstDecoderConfig), ""); void MyPrint(SherpaOnnxOnlineRecognizerConfig *config) { @@ -67,6 +69,11 @@ void MyPrint(SherpaOnnxOnlineRecognizerConfig *config) { config->rule3_min_utterance_length); fprintf(stdout, "hotwords_file: %s\n", config->hotwords_file); fprintf(stdout, "hotwords_score: %.2f\n", config->hotwords_score); + + fprintf(stdout, "----------ctc fst decoder config----------\n"); + fprintf(stdout, "graph: %s\n", config->ctc_fst_decoder_config.graph); + fprintf(stdout, "max_active: %d\n", + config->ctc_fst_decoder_config.max_active); } void CopyHeap(const char *src, int32_t num_bytes, char *dst) { From c1c0f5bafd9911a4eced5e23f3054adc307a8bc7 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Fri, 5 Apr 2024 20:24:27 +0800 Subject: [PATCH 04/45] return timestamps for WebAssembly (#737) --- CMakeLists.txt | 2 +- build-aarch64-linux-gnu.sh | 2 +- build-arm-linux-gnueabihf.sh | 2 +- build-wasm-simd-nodejs.sh | 2 +- nodejs-examples/test-offline-nemo-ctc.js | 2 +- nodejs-examples/test-offline-paraformer.js | 2 +- nodejs-examples/test-offline-transducer.js | 2 +- nodejs-examples/test-offline-whisper.js | 2 +- .../test-online-paraformer-microphone.js | 2 +- nodejs-examples/test-online-paraformer.js | 2 +- .../test-online-transducer-microphone.js | 2 +- nodejs-examples/test-online-transducer.js | 2 +- .../test-online-zipformer2-ctc-hlg.js | 2 +- nodejs-examples/test-online-zipformer2-ctc.js | 2 +- sherpa-onnx/c-api/c-api.cc | 29 ++++++++++++++++++- sherpa-onnx/c-api/c-api.h | 20 ++++++++++++- wasm/asr/CMakeLists.txt | 4 +++ wasm/asr/app-asr.js | 2 +- wasm/asr/sherpa-onnx-asr.js | 23 ++++++++------- wasm/nodejs/CMakeLists.txt | 16 ++++++---- 20 files changed, 88 insertions(+), 34 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1784687dc..6a9fd5785 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.13 FATAL_ERROR) project(sherpa-onnx) -set(SHERPA_ONNX_VERSION "1.9.15") +set(SHERPA_ONNX_VERSION "1.9.16") # Disable warning about # diff --git a/build-aarch64-linux-gnu.sh b/build-aarch64-linux-gnu.sh index 164182c79..36fd9eadd 100755 --- a/build-aarch64-linux-gnu.sh +++ b/build-aarch64-linux-gnu.sh @@ -57,7 +57,7 @@ cmake \ -DSHERPA_ONNX_ENABLE_CHECK=OFF \ -DSHERPA_ONNX_ENABLE_PORTAUDIO=OFF \ -DSHERPA_ONNX_ENABLE_JNI=OFF \ - -DSHERPA_ONNX_ENABLE_C_API=OFF \ + -DSHERPA_ONNX_ENABLE_C_API=ON \ -DSHERPA_ONNX_ENABLE_WEBSOCKET=OFF \ -DCMAKE_TOOLCHAIN_FILE=../toolchains/aarch64-linux-gnu.toolchain.cmake \ .. diff --git a/build-arm-linux-gnueabihf.sh b/build-arm-linux-gnueabihf.sh index 06db39cb0..f6221f295 100755 --- a/build-arm-linux-gnueabihf.sh +++ b/build-arm-linux-gnueabihf.sh @@ -52,7 +52,7 @@ cmake \ -DSHERPA_ONNX_ENABLE_CHECK=OFF \ -DSHERPA_ONNX_ENABLE_PORTAUDIO=OFF \ -DSHERPA_ONNX_ENABLE_JNI=OFF \ - -DSHERPA_ONNX_ENABLE_C_API=OFF \ + -DSHERPA_ONNX_ENABLE_C_API=ON \ -DSHERPA_ONNX_ENABLE_WEBSOCKET=OFF \ -DCMAKE_TOOLCHAIN_FILE=../toolchains/arm-linux-gnueabihf.toolchain.cmake \ .. diff --git a/build-wasm-simd-nodejs.sh b/build-wasm-simd-nodejs.sh index 21a3b25da..3ad88d5d4 100755 --- a/build-wasm-simd-nodejs.sh +++ b/build-wasm-simd-nodejs.sh @@ -57,7 +57,7 @@ cmake \ -DSHERPA_ONNX_ENABLE_BINARY=OFF \ -DSHERPA_ONNX_LINK_LIBSTDCPP_STATICALLY=OFF \ .. -make -j10 +make -j3 make install ls -lh install/bin/wasm/nodejs diff --git a/nodejs-examples/test-offline-nemo-ctc.js b/nodejs-examples/test-offline-nemo-ctc.js index c657fd239..e71f43152 100644 --- a/nodejs-examples/test-offline-nemo-ctc.js +++ b/nodejs-examples/test-offline-nemo-ctc.js @@ -100,7 +100,7 @@ fs.createReadStream(waveFilename, {highWaterMark: 4096}) stream.acceptWaveform(recognizer.config.featConfig.sampleRate, flattened); recognizer.decode(stream); - const text = recognizer.getResult(stream); + const text = recognizer.getResult(stream).text; console.log(text); stream.free(); diff --git a/nodejs-examples/test-offline-paraformer.js b/nodejs-examples/test-offline-paraformer.js index 175b227e6..f1f55bc37 100644 --- a/nodejs-examples/test-offline-paraformer.js +++ b/nodejs-examples/test-offline-paraformer.js @@ -100,7 +100,7 @@ fs.createReadStream(waveFilename, {'highWaterMark': 4096}) stream.acceptWaveform(recognizer.config.featConfig.sampleRate, flattened); recognizer.decode(stream); - const text = recognizer.getResult(stream); + const text = recognizer.getResult(stream).text; console.log(text); stream.free(); diff --git a/nodejs-examples/test-offline-transducer.js b/nodejs-examples/test-offline-transducer.js index 289c01dc0..bb5d4e845 100644 --- a/nodejs-examples/test-offline-transducer.js +++ b/nodejs-examples/test-offline-transducer.js @@ -101,7 +101,7 @@ fs.createReadStream(waveFilename, {'highWaterMark': 4096}) stream.acceptWaveform(recognizer.config.featConfig.sampleRate, flattened); recognizer.decode(stream); - const text = recognizer.getResult(stream); + const text = recognizer.getResult(stream).text; console.log(text); stream.free(); diff --git a/nodejs-examples/test-offline-whisper.js b/nodejs-examples/test-offline-whisper.js index 28b101aed..ab84e6ccf 100644 --- a/nodejs-examples/test-offline-whisper.js +++ b/nodejs-examples/test-offline-whisper.js @@ -100,7 +100,7 @@ fs.createReadStream(waveFilename, {'highWaterMark': 4096}) stream.acceptWaveform(recognizer.config.featConfig.sampleRate, flattened); recognizer.decode(stream); - const text = recognizer.getResult(stream); + const text = recognizer.getResult(stream).text; console.log(text); stream.free(); diff --git a/nodejs-examples/test-online-paraformer-microphone.js b/nodejs-examples/test-online-paraformer-microphone.js index 591b4dbb5..072276468 100644 --- a/nodejs-examples/test-online-paraformer-microphone.js +++ b/nodejs-examples/test-online-paraformer-microphone.js @@ -86,7 +86,7 @@ ai.on('data', data => { } const isEndpoint = recognizer.isEndpoint(stream); - const text = recognizer.getResult(stream); + const text = recognizer.getResult(stream).text; if (text.length > 0 && lastText != text) { lastText = text; diff --git a/nodejs-examples/test-online-paraformer.js b/nodejs-examples/test-online-paraformer.js index 01b8feeb9..5d1eae166 100644 --- a/nodejs-examples/test-online-paraformer.js +++ b/nodejs-examples/test-online-paraformer.js @@ -75,7 +75,7 @@ function decode(samples) { while (recognizer.isReady(stream)) { recognizer.decode(stream); } - const text = recognizer.getResult(stream); + const text = recognizer.getResult(stream).text; console.log(text); } diff --git a/nodejs-examples/test-online-transducer-microphone.js b/nodejs-examples/test-online-transducer-microphone.js index 6312b5679..52eba8a99 100644 --- a/nodejs-examples/test-online-transducer-microphone.js +++ b/nodejs-examples/test-online-transducer-microphone.js @@ -88,7 +88,7 @@ ai.on('data', data => { } const isEndpoint = recognizer.isEndpoint(stream); - const text = recognizer.getResult(stream); + const text = recognizer.getResult(stream).text; if (text.length > 0 && lastText != text) { lastText = text; diff --git a/nodejs-examples/test-online-transducer.js b/nodejs-examples/test-online-transducer.js index e4bb46d2e..2ca30ee2b 100644 --- a/nodejs-examples/test-online-transducer.js +++ b/nodejs-examples/test-online-transducer.js @@ -77,7 +77,7 @@ function decode(samples) { while (recognizer.isReady(stream)) { recognizer.decode(stream); } - const text = recognizer.getResult(stream); + const text = recognizer.getResult(stream).text; console.log(text); } diff --git a/nodejs-examples/test-online-zipformer2-ctc-hlg.js b/nodejs-examples/test-online-zipformer2-ctc-hlg.js index 1bf999927..fed7b4e5c 100644 --- a/nodejs-examples/test-online-zipformer2-ctc-hlg.js +++ b/nodejs-examples/test-online-zipformer2-ctc-hlg.js @@ -74,7 +74,7 @@ function decode(samples) { while (recognizer.isReady(stream)) { recognizer.decode(stream); } - const text = recognizer.getResult(stream); + const text = recognizer.getResult(stream).text; console.log(text); } diff --git a/nodejs-examples/test-online-zipformer2-ctc.js b/nodejs-examples/test-online-zipformer2-ctc.js index 2e85d69a0..ad239accd 100644 --- a/nodejs-examples/test-online-zipformer2-ctc.js +++ b/nodejs-examples/test-online-zipformer2-ctc.js @@ -75,7 +75,7 @@ function decode(samples) { while (recognizer.isReady(stream)) { recognizer.decode(stream); } - const text = recognizer.getResult(stream); + const text = recognizer.getResult(stream).text; console.log(text); } diff --git a/sherpa-onnx/c-api/c-api.cc b/sherpa-onnx/c-api/c-api.cc index 8baecd06b..9292687af 100644 --- a/sherpa-onnx/c-api/c-api.cc +++ b/sherpa-onnx/c-api/c-api.cc @@ -243,6 +243,20 @@ void DestroyOnlineRecognizerResult(const SherpaOnnxOnlineRecognizerResult *r) { } } +const char *GetOnlineStreamResultAsJson( + const SherpaOnnxOnlineRecognizer *recognizer, + const SherpaOnnxOnlineStream *stream) { + sherpa_onnx::OnlineRecognizerResult result = + recognizer->impl->GetResult(stream->impl.get()); + std::string json = result.AsJsonString(); + char *pJson = new char[json.size() + 1]; + std::copy(json.begin(), json.end(), pJson); + pJson[json.size()] = 0; + return pJson; +} + +void DestroyOnlineStreamResultJson(const char *s) { delete[] s; } + void Reset(const SherpaOnnxOnlineRecognizer *recognizer, const SherpaOnnxOnlineStream *stream) { recognizer->impl->Reset(stream->impl.get()); @@ -409,7 +423,7 @@ void DecodeMultipleOfflineStreams(SherpaOnnxOfflineRecognizer *recognizer, } const SherpaOnnxOfflineRecognizerResult *GetOfflineStreamResult( - SherpaOnnxOfflineStream *stream) { + const SherpaOnnxOfflineStream *stream) { const sherpa_onnx::OfflineRecognitionResult &result = stream->impl->GetResult(); const auto &text = result.text; @@ -444,6 +458,19 @@ void DestroyOfflineRecognizerResult( } } +const char *GetOfflineStreamResultAsJson( + const SherpaOnnxOfflineStream *stream) { + const sherpa_onnx::OfflineRecognitionResult &result = + stream->impl->GetResult(); + std::string json = result.AsJsonString(); + char *pJson = new char[json.size() + 1]; + std::copy(json.begin(), json.end(), pJson); + pJson[json.size()] = 0; + return pJson; +} + +void DestroyOfflineStreamResultJson(const char *s) { delete[] s; } + // ============================================================ // For Keyword Spot // ============================================================ diff --git a/sherpa-onnx/c-api/c-api.h b/sherpa-onnx/c-api/c-api.h index 55ad46632..78641f9bf 100644 --- a/sherpa-onnx/c-api/c-api.h +++ b/sherpa-onnx/c-api/c-api.h @@ -286,6 +286,16 @@ SHERPA_ONNX_API const SherpaOnnxOnlineRecognizerResult *GetOnlineStreamResult( SHERPA_ONNX_API void DestroyOnlineRecognizerResult( const SherpaOnnxOnlineRecognizerResult *r); +/// Return the result as a json string. +/// The user has to invoke +/// DestroyOnlineStreamResultJson() +/// to free the returned pointer to avoid memory leak +SHERPA_ONNX_API const char *GetOnlineStreamResultAsJson( + const SherpaOnnxOnlineRecognizer *recognizer, + const SherpaOnnxOnlineStream *stream); + +SHERPA_ONNX_API void DestroyOnlineStreamResultJson(const char *s); + /// Reset an OnlineStream , which clears the neural network model state /// and the state for decoding. /// @@ -482,7 +492,7 @@ SHERPA_ONNX_API typedef struct SherpaOnnxOfflineRecognizerResult { /// DestroyOnlineRecognizerResult() to free the returned pointer to /// avoid memory leak. SHERPA_ONNX_API const SherpaOnnxOfflineRecognizerResult *GetOfflineStreamResult( - SherpaOnnxOfflineStream *stream); + const SherpaOnnxOfflineStream *stream); /// Destroy the pointer returned by GetOfflineStreamResult(). /// @@ -490,6 +500,14 @@ SHERPA_ONNX_API const SherpaOnnxOfflineRecognizerResult *GetOfflineStreamResult( SHERPA_ONNX_API void DestroyOfflineRecognizerResult( const SherpaOnnxOfflineRecognizerResult *r); +/// Return the result as a json string. +/// The user has to use DestroyOfflineStreamResultJson() +/// to free the returned pointer to avoid memory leak +SHERPA_ONNX_API const char *GetOfflineStreamResultAsJson( + const SherpaOnnxOfflineStream *stream); + +SHERPA_ONNX_API void DestroyOfflineStreamResultJson(const char *s); + // ============================================================ // For Keyword Spot // ============================================================ diff --git a/wasm/asr/CMakeLists.txt b/wasm/asr/CMakeLists.txt index 876c6471e..b46fe39a1 100644 --- a/wasm/asr/CMakeLists.txt +++ b/wasm/asr/CMakeLists.txt @@ -13,10 +13,14 @@ set(exported_functions CreateOnlineRecognizer CreateOnlineStream DecodeOnlineStream + DestroyOfflineStreamResultJson DestroyOnlineRecognizer DestroyOnlineRecognizerResult DestroyOnlineStream + DestroyOnlineStreamResultJson + GetOfflineStreamResultAsJson GetOnlineStreamResult + GetOnlineStreamResultAsJson InputFinished IsEndpoint IsOnlineStreamReady diff --git a/wasm/asr/app-asr.js b/wasm/asr/app-asr.js index 0f6ec257e..86836d5c3 100644 --- a/wasm/asr/app-asr.js +++ b/wasm/asr/app-asr.js @@ -108,7 +108,7 @@ if (navigator.mediaDevices.getUserMedia) { } let isEndpoint = recognizer.isEndpoint(recognizer_stream); - let result = recognizer.getResult(recognizer_stream); + let result = recognizer.getResult(recognizer_stream).text; if (result.length > 0 && lastResult != result) { diff --git a/wasm/asr/sherpa-onnx-asr.js b/wasm/asr/sherpa-onnx-asr.js index e61757cf9..6b66b73b5 100644 --- a/wasm/asr/sherpa-onnx-asr.js +++ b/wasm/asr/sherpa-onnx-asr.js @@ -661,13 +661,12 @@ class OfflineRecognizer { } getResult(stream) { - const r = this.Module._GetOfflineStreamResult(stream.handle); + const r = this.Module._GetOfflineStreamResultAsJson(stream.handle); + const jsonStr = this.Module.UTF8ToString(r); + const ans = JSON.parse(jsonStr); + this.Module._DestroyOfflineStreamResultJson(r); - const textPtr = this.Module.getValue(r, 'i8*'); - const text = this.Module.UTF8ToString(textPtr); - - this.Module._DestroyOfflineRecognizerResult(r); - return text; + return ans; } }; @@ -750,11 +749,13 @@ class OnlineRecognizer { } getResult(stream) { - const r = this.Module._GetOnlineStreamResult(this.handle, stream.handle); - const textPtr = this.Module.getValue(r, 'i8*'); - const text = this.Module.UTF8ToString(textPtr); - this.Module._DestroyOnlineRecognizerResult(r); - return text; + const r = + this.Module._GetOnlineStreamResultAsJson(this.handle, stream.handle); + const jsonStr = this.Module.UTF8ToString(r); + const ans = JSON.parse(jsonStr); + this.Module._DestroyOnlineStreamResultJson(r); + + return ans; } } diff --git a/wasm/nodejs/CMakeLists.txt b/wasm/nodejs/CMakeLists.txt index f90387e9b..7d1595d4b 100644 --- a/wasm/nodejs/CMakeLists.txt +++ b/wasm/nodejs/CMakeLists.txt @@ -21,22 +21,26 @@ set(exported_functions DestroyOnlineRecognizer DestroyOnlineRecognizerResult DestroyOnlineStream + DestroyOnlineStreamResultJson GetOnlineStreamResult + GetOnlineStreamResultAsJson InputFinished IsEndpoint IsOnlineStreamReady Reset # non-streaming ASR - PrintOfflineRecognizerConfig + AcceptWaveformOffline CreateOfflineRecognizer - DestroyOfflineRecognizer CreateOfflineStream - DestroyOfflineStream - AcceptWaveformOffline - DecodeOfflineStream DecodeMultipleOfflineStreams - GetOfflineStreamResult + DecodeOfflineStream + DestroyOfflineRecognizer DestroyOfflineRecognizerResult + DestroyOfflineStream + DestroyOfflineStreamResultJson + GetOfflineStreamResult + GetOfflineStreamResultAsJson + PrintOfflineRecognizerConfig # online kws CreateKeywordSpotter DestroyKeywordSpotter From a5f8fbc83fb0ee1c977d078e532adc4ec64f59d0 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Mon, 8 Apr 2024 11:01:30 +0800 Subject: [PATCH 05/45] Support heteronyms in Chinese TTS (#738) --- .github/scripts/test-nodejs-npm.sh | 8 +- .github/workflows/arm-linux-gnueabihf.yaml | 1 + .github/workflows/riscv64-linux.yaml | 1 + .github/workflows/test-go.yaml | 6 +- .gitignore | 1 + CMakeLists.txt | 2 +- .../com/k2fsa/sherpa/onnx/MainActivity.kt | 13 ++- .../main/java/com/k2fsa/sherpa/onnx/Tts.kt | 5 +- .../k2fsa/sherpa/onnx/tts/engine/TtsEngine.kt | 12 ++- build-ios.sh | 3 + build-swift-macos.sh | 1 + c-api-examples/Makefile | 2 +- cmake/cmake_extension.py | 1 + cmake/kaldi-decoder.cmake | 15 +++ cmake/kaldifst.cmake | 6 -- cmake/openfst.cmake | 27 +++--- cmake/sherpa-onnx.pc.in | 2 +- dotnet-examples/offline-tts/Program.cs | 17 ++-- dotnet-examples/offline-tts/run-aishell3.sh | 18 ++-- go-api-examples/non-streaming-tts/main.go | 1 + .../non-streaming-tts/run-vits-zh-aishell3.sh | 25 +++-- .../SherpaOnnxTts/ViewModel.swift | 22 +++-- .../sherpa-onnx-deps.props | 1 + .../sherpa-onnx-deps.props | 1 + .../sherpa-onnx-deps.props | 1 + nodejs-examples/README.md | 4 +- nodejs-examples/test-offline-tts-en.js | 1 + nodejs-examples/test-offline-tts-zh.js | 10 +- scripts/apk/build-apk-tts-engine.sh.in | 5 + scripts/apk/build-apk-tts.sh.in | 5 + scripts/apk/generate-tts-apk-script.py | 31 +++---- scripts/dotnet/generate.py | 3 + scripts/dotnet/offline.cs | 4 + scripts/dotnet/run.sh | 2 + scripts/go/_internal/build_darwin_amd64.go | 2 +- scripts/go/sherpa_onnx.go | 4 + sherpa-onnx/c-api/c-api.cc | 1 + sherpa-onnx/c-api/c-api.h | 1 + sherpa-onnx/csrc/CMakeLists.txt | 1 + sherpa-onnx/csrc/lexicon.cc | 93 ++++++++++--------- sherpa-onnx/csrc/lexicon.h | 4 - sherpa-onnx/csrc/offline-tts-vits-impl.h | 29 ++++++ sherpa-onnx/csrc/offline-tts.cc | 21 ++++- sherpa-onnx/csrc/offline-tts.h | 7 +- sherpa-onnx/jni/jni.cc | 7 ++ sherpa-onnx/python/csrc/offline-tts.cc | 5 +- swift-api-examples/SherpaOnnx.swift | 4 +- wasm/tts/sherpa-onnx-tts.js | 12 ++- wasm/tts/sherpa-onnx-wasm-main-tts.cc | 3 +- 49 files changed, 308 insertions(+), 143 deletions(-) diff --git a/.github/scripts/test-nodejs-npm.sh b/.github/scripts/test-nodejs-npm.sh index 1531aff2c..95dcf0271 100755 --- a/.github/scripts/test-nodejs-npm.sh +++ b/.github/scripts/test-nodejs-npm.sh @@ -70,9 +70,9 @@ rm -rf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18 curl -LS -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-piper-en_US-amy-low.tar.bz2 tar xf vits-piper-en_US-amy-low.tar.bz2 node ./test-offline-tts-en.js -rm vits-piper-en_US-amy-low.tar.bz2 +rm vits-piper-en_US-amy-low* -curl -LS -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-zh-aishell3.tar.bz2 -tar xvf vits-zh-aishell3.tar.bz2 +curl -LS -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-icefall-zh-aishell3.tar.bz2 +tar xvf vits-icefall-zh-aishell3.tar.bz2 node ./test-offline-tts-zh.js -rm vits-zh-aishell3.tar.bz2 +rm vits-icefall-zh-aishell3* diff --git a/.github/workflows/arm-linux-gnueabihf.yaml b/.github/workflows/arm-linux-gnueabihf.yaml index 76ad9fcf8..762239880 100644 --- a/.github/workflows/arm-linux-gnueabihf.yaml +++ b/.github/workflows/arm-linux-gnueabihf.yaml @@ -173,6 +173,7 @@ jobs: rm -v $dst/lib/libasound.so rm -v $dst/lib/libonnxruntime.so rm -v $dst/lib/libsherpa-onnx-fst.so + rm -v $dst/lib/libsherpa-onnx-fstfar.so fi tree $dst diff --git a/.github/workflows/riscv64-linux.yaml b/.github/workflows/riscv64-linux.yaml index b1008b514..a5869a4b0 100644 --- a/.github/workflows/riscv64-linux.yaml +++ b/.github/workflows/riscv64-linux.yaml @@ -211,6 +211,7 @@ jobs: rm -fv $dst/lib/libasound.so rm -fv $dst/lib/libonnxruntime.so rm -fv $dst/lib/libsherpa-onnx-fst.so + rm -fv $dst/lib/libsherpa-onnx-fstfar.so fi tree $dst diff --git a/.github/workflows/test-go.yaml b/.github/workflows/test-go.yaml index 17af77e62..e7bf9cfde 100644 --- a/.github/workflows/test-go.yaml +++ b/.github/workflows/test-go.yaml @@ -111,9 +111,11 @@ jobs: rm -rf vits-vctk echo "Test vits-zh-aishell3" - git clone https://huggingface.co/csukuangfj/vits-zh-aishell3 + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-icefall-zh-aishell3.tar.bz2 + tar xvf vits-icefall-zh-aishell3.tar.bz2 + rm vits-icefall-zh-aishell3.tar.bz2 ./run-vits-zh-aishell3.sh - rm -rf vits-zh-aishell3 + rm -rf vits-icefall-zh-aishell3 echo "Test vits-piper-en_US-lessac-medium" git clone https://huggingface.co/csukuangfj/vits-piper-en_US-lessac-medium diff --git a/.gitignore b/.gitignore index c2c874243..a51cd0ea3 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,4 @@ sherpa-onnx-paraformer-trilingual-zh-cantonese-en sr-data *xcworkspace/xcuserdata/* +vits-icefall-* diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a9fd5785..670b4b3e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.13 FATAL_ERROR) project(sherpa-onnx) -set(SHERPA_ONNX_VERSION "1.9.16") +set(SHERPA_ONNX_VERSION "1.9.17") # Disable warning about # diff --git a/android/SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt b/android/SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt index 86c565e3b..9f8e6325f 100644 --- a/android/SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt +++ b/android/SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt @@ -155,6 +155,7 @@ class MainActivity : AppCompatActivity() { var modelDir: String? var modelName: String? var ruleFsts: String? + var ruleFars: String? var lexicon: String? var dataDir: String? var assets: AssetManager? = application.assets @@ -165,6 +166,7 @@ class MainActivity : AppCompatActivity() { modelDir = null modelName = null ruleFsts = null + ruleFars = null lexicon = null dataDir = null @@ -181,9 +183,11 @@ class MainActivity : AppCompatActivity() { // dataDir = "vits-piper-en_US-amy-low/espeak-ng-data" // Example 3: - // modelDir = "vits-zh-aishell3" - // modelName = "vits-aishell3.onnx" - // ruleFsts = "vits-zh-aishell3/rule.fst" + // https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-icefall-zh-aishell3.tar.bz2 + // modelDir = "vits-icefall-zh-aishell3" + // modelName = "model.onnx" + // ruleFsts = "vits-icefall-zh-aishell3/phone.fst,vits-icefall-zh-aishell3/date.fst,vits-icefall-zh-aishell3/number.fst," + // ruleFars = "vits-icefall-zh-aishell3/rule.far" // lexicon = "lexicon.txt" // Example 4: @@ -202,7 +206,8 @@ class MainActivity : AppCompatActivity() { val config = getOfflineTtsConfig( modelDir = modelDir!!, modelName = modelName!!, lexicon = lexicon ?: "", dataDir = dataDir ?: "", - ruleFsts = ruleFsts ?: "" + ruleFsts = ruleFsts ?: "", + ruleFars = ruleFars ?: "", )!! tts = OfflineTts(assetManager = assets, config = config) diff --git a/android/SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/Tts.kt b/android/SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/Tts.kt index be48b6db8..2514fcac5 100644 --- a/android/SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/Tts.kt +++ b/android/SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/Tts.kt @@ -23,6 +23,7 @@ data class OfflineTtsModelConfig( data class OfflineTtsConfig( var model: OfflineTtsModelConfig, var ruleFsts: String = "", + var ruleFars: String = "", var maxNumSentences: Int = 1, ) @@ -151,7 +152,8 @@ fun getOfflineTtsConfig( modelName: String, lexicon: String, dataDir: String, - ruleFsts: String + ruleFsts: String, + ruleFars: String ): OfflineTtsConfig? { return OfflineTtsConfig( model = OfflineTtsModelConfig( @@ -166,5 +168,6 @@ fun getOfflineTtsConfig( provider = "cpu", ), ruleFsts = ruleFsts, + ruleFars = ruleFars, ) } diff --git a/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/TtsEngine.kt b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/TtsEngine.kt index f814a2e0f..5699ccf20 100644 --- a/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/TtsEngine.kt +++ b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/TtsEngine.kt @@ -39,6 +39,7 @@ object TtsEngine { private var modelDir: String? = null private var modelName: String? = null private var ruleFsts: String? = null + private var ruleFars: String? = null private var lexicon: String? = null private var dataDir: String? = null private var assets: AssetManager? = null @@ -50,6 +51,7 @@ object TtsEngine { modelDir = null modelName = null ruleFsts = null + ruleFars = null lexicon = null dataDir = null lang = null @@ -73,9 +75,10 @@ object TtsEngine { // Example 3: // https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-icefall-zh-aishell3.tar.bz2 - // modelDir = "vits-zh-aishell3" - // modelName = "vits-aishell3.onnx" - // ruleFsts = "vits-zh-aishell3/rule.fst" + // modelDir = "vits-icefall-zh-aishell3" + // modelName = "model.onnx" + // ruleFsts = "vits-icefall-zh-aishell3/phone.fst,vits-icefall-zh-aishell3/date.fst,vits-icefall-zh-aishell3/number.fst,vits-icefall-zh-aishell3/new_heteronym.fst" + // ruleFars = "vits-icefall-zh-aishell3/rule.far" // lexicon = "lexicon.txt" // lang = "zho" @@ -108,7 +111,8 @@ object TtsEngine { val config = getOfflineTtsConfig( modelDir = modelDir!!, modelName = modelName!!, lexicon = lexicon ?: "", dataDir = dataDir ?: "", - ruleFsts = ruleFsts ?: "" + ruleFsts = ruleFsts ?: "", + ruleFars = ruleFars ?: "" )!! tts = OfflineTts(assetManager = assets, config = config) diff --git a/build-ios.sh b/build-ios.sh index 599a1725f..a687dc4de 100755 --- a/build-ios.sh +++ b/build-ios.sh @@ -124,6 +124,7 @@ echo "Generate xcframework" mkdir -p "build/simulator/lib" for f in libkaldi-native-fbank-core.a libsherpa-onnx-c-api.a libsherpa-onnx-core.a \ + libsherpa-onnx-fstfar.a \ libsherpa-onnx-fst.a libsherpa-onnx-kaldifst-core.a libkaldi-decoder-core.a \ libucd.a libpiper_phonemize.a libespeak-ng.a; do lipo -create build/simulator_arm64/lib/${f} \ @@ -137,6 +138,7 @@ libtool -static -o build/simulator/sherpa-onnx.a \ build/simulator/lib/libkaldi-native-fbank-core.a \ build/simulator/lib/libsherpa-onnx-c-api.a \ build/simulator/lib/libsherpa-onnx-core.a \ + build/simulator/lib/libsherpa-onnx-fstfar.a \ build/simulator/lib/libsherpa-onnx-fst.a \ build/simulator/lib/libsherpa-onnx-kaldifst-core.a \ build/simulator/lib/libkaldi-decoder-core.a \ @@ -148,6 +150,7 @@ libtool -static -o build/os64/sherpa-onnx.a \ build/os64/lib/libkaldi-native-fbank-core.a \ build/os64/lib/libsherpa-onnx-c-api.a \ build/os64/lib/libsherpa-onnx-core.a \ + build/os64/lib/libsherpa-onnx-fstfar.a \ build/os64/lib/libsherpa-onnx-fst.a \ build/os64/lib/libsherpa-onnx-kaldifst-core.a \ build/os64/lib/libkaldi-decoder-core.a \ diff --git a/build-swift-macos.sh b/build-swift-macos.sh index e5cdbc086..1b1867c54 100755 --- a/build-swift-macos.sh +++ b/build-swift-macos.sh @@ -27,6 +27,7 @@ libtool -static -o ./install/lib/libsherpa-onnx.a \ ./install/lib/libsherpa-onnx-c-api.a \ ./install/lib/libsherpa-onnx-core.a \ ./install/lib/libkaldi-native-fbank-core.a \ + ./install/lib/libsherpa-onnx-fstfar.a \ ./install/lib/libsherpa-onnx-fst.a \ ./install/lib/libsherpa-onnx-kaldifst-core.a \ ./install/lib/libkaldi-decoder-core.a \ diff --git a/c-api-examples/Makefile b/c-api-examples/Makefile index 3e2931424..40d35d866 100644 --- a/c-api-examples/Makefile +++ b/c-api-examples/Makefile @@ -4,7 +4,7 @@ CUR_DIR :=$(shell pwd) CFLAGS := -I ../ -I ../build/_deps/cargs-src/include/ LDFLAGS := -L ../build/lib LDFLAGS += -L ../build/_deps/onnxruntime-src/lib -LDFLAGS += -lsherpa-onnx-c-api -lsherpa-onnx-core -lkaldi-decoder-core -lsherpa-onnx-kaldifst-core -lsherpa-onnx-fst -lkaldi-native-fbank-core -lpiper_phonemize -lespeak-ng -lucd -lcargs -lonnxruntime +LDFLAGS += -lsherpa-onnx-c-api -lsherpa-onnx-core -lkaldi-decoder-core -lsherpa-onnx-kaldifst-core -lsherpa-onnx-fstfar -lsherpa-onnx-fst -lkaldi-native-fbank-core -lpiper_phonemize -lespeak-ng -lucd -lcargs -lonnxruntime LDFLAGS += -framework Foundation LDFLAGS += -lc++ LDFLAGS += -Wl,-rpath,${CUR_DIR}/../build/lib diff --git a/cmake/cmake_extension.py b/cmake/cmake_extension.py index 7fb0fc0f2..ea52bdd64 100644 --- a/cmake/cmake_extension.py +++ b/cmake/cmake_extension.py @@ -78,6 +78,7 @@ def get_binaries(): "piper_phonemize.dll", "sherpa-onnx-c-api.dll", "sherpa-onnx-core.dll", + "sherpa-onnx-fstfar.lib", "sherpa-onnx-fst.lib", "sherpa-onnx-kaldifst-core.lib", "sherpa-onnx-portaudio.dll", diff --git a/cmake/kaldi-decoder.cmake b/cmake/kaldi-decoder.cmake index 99ebf9aa0..b78ece58f 100644 --- a/cmake/kaldi-decoder.cmake +++ b/cmake/kaldi-decoder.cmake @@ -64,12 +64,22 @@ function(download_kaldi_decoder) kaldifst_core fst DESTINATION ..) + if(SHERPA_ONNX_ENABLE_TTS) + install(TARGETS + fstfar + DESTINATION ..) + endif() else() install(TARGETS kaldi-decoder-core kaldifst_core fst DESTINATION lib) + if(SHERPA_ONNX_ENABLE_TTS) + install(TARGETS + fstfar + DESTINATION lib) + endif() endif() if(WIN32 AND BUILD_SHARED_LIBS) @@ -78,6 +88,11 @@ function(download_kaldi_decoder) kaldifst_core fst DESTINATION bin) + if(SHERPA_ONNX_ENABLE_TTS) + install(TARGETS + fstfar + DESTINATION bin) + endif() endif() endfunction() diff --git a/cmake/kaldifst.cmake b/cmake/kaldifst.cmake index 12bcc0301..3b5ce3ba2 100644 --- a/cmake/kaldifst.cmake +++ b/cmake/kaldifst.cmake @@ -50,13 +50,7 @@ function(download_kaldifst) ${kaldifst_SOURCE_DIR}/ ) - target_include_directories(fst - PUBLIC - ${openfst_SOURCE_DIR}/src/include - ) - set_target_properties(kaldifst_core PROPERTIES OUTPUT_NAME "sherpa-onnx-kaldifst-core") - set_target_properties(fst PROPERTIES OUTPUT_NAME "sherpa-onnx-fst") endfunction() download_kaldifst() diff --git a/cmake/openfst.cmake b/cmake/openfst.cmake index 34073d2cd..575ea8aed 100644 --- a/cmake/openfst.cmake +++ b/cmake/openfst.cmake @@ -4,7 +4,7 @@ function(download_openfst) include(FetchContent) set(openfst_URL "https://github.com/kkm000/openfst/archive/refs/tags/win/1.6.5.1.tar.gz") - set(openfst_URL2 "https://huggingface.co/csukuangfj/kaldi-hmm-gmm-cmake-deps/resolve/main/openfst-win-1.6.5.1.tar.gz") + set(openfst_URL2 "https://hub.nuaa.cf/kkm000/openfst/archive/refs/tags/win/1.6.5.1.tar.gz") set(openfst_HASH "SHA256=02c49b559c3976a536876063369efc0e41ab374be1035918036474343877046e") # If you don't have access to the Internet, @@ -31,7 +31,7 @@ function(download_openfst) set(HAVE_COMPACT OFF CACHE BOOL "" FORCE) set(HAVE_COMPRESS OFF CACHE BOOL "" FORCE) set(HAVE_CONST OFF CACHE BOOL "" FORCE) - set(HAVE_FAR OFF CACHE BOOL "" FORCE) + set(HAVE_FAR ON CACHE BOOL "" FORCE) set(HAVE_GRM OFF CACHE BOOL "" FORCE) set(HAVE_PDT OFF CACHE BOOL "" FORCE) set(HAVE_MPDT OFF CACHE BOOL "" FORCE) @@ -70,20 +70,21 @@ function(download_openfst) add_subdirectory(${openfst_SOURCE_DIR} ${openfst_BINARY_DIR} EXCLUDE_FROM_ALL) set(openfst_SOURCE_DIR ${openfst_SOURCE_DIR} PARENT_SCOPE) - # Rename libfst.so.6 to libkaldifst_fst.so.6 to avoid potential conflicts - # when kaldifst is installed. - set_target_properties(fst PROPERTIES OUTPUT_NAME "kaldifst_fst") + # Rename libfst.so.6 to libsherpa-onnx-fst.so.6 to avoid potential conflicts + # when sherpa-onnx is installed. + set_target_properties(fst PROPERTIES OUTPUT_NAME "sherpa-onnx-fst") + set_target_properties(fstfar PROPERTIES OUTPUT_NAME "sherpa-onnx-fstfar") - install(TARGETS fst - DESTINATION lib + target_include_directories(fst + PUBLIC + ${openfst_SOURCE_DIR}/src/include ) - if(KALDIFST_BUILD_PYTHON) - set_target_properties(fstscript PROPERTIES OUTPUT_NAME "kaldifst_fstscript") - install(TARGETS fstscript - DESTINATION lib - ) - endif() + target_include_directories(fstfar + PUBLIC + ${openfst_SOURCE_DIR}/src/include + ) + # installed in ./kaldi-decoder.cmake endfunction() download_openfst() diff --git a/cmake/sherpa-onnx.pc.in b/cmake/sherpa-onnx.pc.in index aae8abab7..0870f3ae7 100644 --- a/cmake/sherpa-onnx.pc.in +++ b/cmake/sherpa-onnx.pc.in @@ -13,4 +13,4 @@ Cflags: -I"${includedir}" # Note: -lcargs is required only for the following file # https://github.com/k2-fsa/sherpa-onnx/blob/master/c-api-examples/decode-file-c-api.c # We add it here so that users don't need to specify -lcargs when compiling decode-file-c-api.c -Libs: -L"${libdir}" -lsherpa-onnx-c-api -lsherpa-onnx-core -lkaldi-decoder-core -lsherpa-onnx-kaldifst-core -lsherpa-onnx-fst -lkaldi-native-fbank-core -lpiper_phonemize -lespeak-ng -lucd -lcargs -lonnxruntime -Wl,-rpath,${libdir} @SHERPA_ONNX_PKG_CONFIG_EXTRA_LIBS@ +Libs: -L"${libdir}" -lsherpa-onnx-c-api -lsherpa-onnx-core -lkaldi-decoder-core -lsherpa-onnx-kaldifst-core -lsherpa-onnx-fstfar -lsherpa-onnx-fst -lkaldi-native-fbank-core -lpiper_phonemize -lespeak-ng -lucd -lcargs -lonnxruntime -Wl,-rpath,${libdir} @SHERPA_ONNX_PKG_CONFIG_EXTRA_LIBS@ diff --git a/dotnet-examples/offline-tts/Program.cs b/dotnet-examples/offline-tts/Program.cs index 497200e88..85fcd2860 100644 --- a/dotnet-examples/offline-tts/Program.cs +++ b/dotnet-examples/offline-tts/Program.cs @@ -20,6 +20,9 @@ class Options [Option("tts-rule-fsts", Required = false, Default = "", HelpText = "path to rule.fst")] public string RuleFsts { get; set; } + [Option("tts-rule-fars", Required = false, Default = "", HelpText = "path to rule.far")] + public string RuleFars { get; set; } + [Option("vits-data-dir", Required = false, Default = "", HelpText = "Path to the directory containing dict for espeak-ng.")] public string DataDir { get; set; } @@ -72,14 +75,15 @@ private static void DisplayHelp(ParserResult result, IEnumerable er string usage = @" # vits-aishell3 -wget -qq https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-zh-aishell3.tar.bz2 -tar xf vits-zh-aishell3.tar.bz2 +curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-icefall-zh-aishell3.tar.bz2 +tar xvf vits-icefall-zh-aishell3.tar.bz2 dotnet run \ - --vits-model=./vits-zh-aishell3/vits-aishell3.onnx \ - --vits-tokens=./vits-zh-aishell3/tokens.txt \ - --vits-lexicon=./vits-zh-aishell3/lexicon.txt \ - --tts-rule-fsts=./vits-zh-aishell3/rule.fst \ + --vits-model=./vits-icefall-zh-aishell3/model.onnx \ + --vits-tokens=./vits-icefall-zh-aishell3/tokens.txt \ + --vits-lexicon=./vits-icefall-zh-aishell3/lexicon.txt \ + --tts-rule-fsts=./vits-icefall-zh-aishell3/phone.fst,./vits-icefall-zh-aishell3/date.fst,./vits-icefall-zh-aishell3/number.fst \ + --tts-rule-fars=./vits-icefall-zh-aishell3/rule.far \ --sid=66 \ --debug=1 \ --output-filename=./aishell3-66.wav \ @@ -127,6 +131,7 @@ private static void Run(Options options) config.Model.Debug = options.Debug; config.Model.Provider = "cpu"; config.RuleFsts = options.RuleFsts; + config.RuleFars = options.RuleFars; config.MaxNumSentences = options.MaxNumSentences; OfflineTts tts = new OfflineTts(config); diff --git a/dotnet-examples/offline-tts/run-aishell3.sh b/dotnet-examples/offline-tts/run-aishell3.sh index 44e5e2619..02380f07c 100755 --- a/dotnet-examples/offline-tts/run-aishell3.sh +++ b/dotnet-examples/offline-tts/run-aishell3.sh @@ -1,18 +1,18 @@ #!/usr/bin/env bash set -ex if [ ! -f ./vits-zh-aishell3/vits-aishell3.onnx ]; then - # wget -qq https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-zh-aishell3.tar.bz2 - curl -OL https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-zh-aishell3.tar.bz2 - tar xf vits-zh-aishell3.tar.bz2 - rm vits-zh-aishell3.tar.bz2 + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-icefall-zh-aishell3.tar.bz2 + tar xvf vits-icefall-zh-aishell3.tar.bz2 + rm vits-icefall-zh-aishell3.tar.bz2 fi dotnet run \ - --vits-model=./vits-zh-aishell3/vits-aishell3.onnx \ - --vits-tokens=./vits-zh-aishell3/tokens.txt \ - --vits-lexicon=./vits-zh-aishell3/lexicon.txt \ - --tts-rule-fsts=./vits-zh-aishell3/rule.fst \ + --vits-model=./vits-icefall-zh-aishell3/model.onnx \ + --vits-tokens=./vits-icefall-zh-aishell3/tokens.txt \ + --vits-lexicon=./vits-icefall-zh-aishell3/lexicon.txt \ + --tts-rule-fsts=./vits-icefall-zh-aishell3/phone.fst,./vits-icefall-zh-aishell3/date.fst,./vits-icefall-zh-aishell3/number.fst \ + --tts-rule-fars=./vits-icefall-zh-aishell3/rule.far \ --sid=66 \ --debug=1 \ --output-filename=./aishell3-66.wav \ - --text="这是一个语音合成测试, 写于公元 2024 年 1 月 28 号, 23点27分,星期天。" + --text="这是一个语音合成测试, 写于公元 2024 年 1 月 28 号, 23点27分,星期天。长沙长大,去过长白山和长安街。行行出状元。行行,银行行长,行业。" diff --git a/go-api-examples/non-streaming-tts/main.go b/go-api-examples/non-streaming-tts/main.go index 8faa605a6..0ddeb8fe4 100644 --- a/go-api-examples/non-streaming-tts/main.go +++ b/go-api-examples/non-streaming-tts/main.go @@ -26,6 +26,7 @@ func main() { flag.IntVar(&config.Model.Debug, "debug", 0, "Whether to show debug message") flag.StringVar(&config.Model.Provider, "provider", "cpu", "Provider to use") flag.StringVar(&config.RuleFsts, "tts-rule-fsts", "", "Path to rule.fst") + flag.StringVar(&config.RuleFars, "tts-rule-fars", "", "Path to rule.far") flag.IntVar(&config.MaxNumSentences, "tts-max-num-sentences", 1, "Batch size") flag.IntVar(&sid, "sid", 0, "Speaker ID. Used only for multi-speaker models") diff --git a/go-api-examples/non-streaming-tts/run-vits-zh-aishell3.sh b/go-api-examples/non-streaming-tts/run-vits-zh-aishell3.sh index 7d592e296..2ab0c6130 100755 --- a/go-api-examples/non-streaming-tts/run-vits-zh-aishell3.sh +++ b/go-api-examples/non-streaming-tts/run-vits-zh-aishell3.sh @@ -6,21 +6,32 @@ for sid in 10 33 99; do ./non-streaming-tts \ - --vits-model=./vits-zh-aishell3/vits-aishell3.onnx \ - --vits-lexicon=./vits-zh-aishell3/lexicon.txt \ - --vits-tokens=./vits-zh-aishell3/tokens.txt \ + --vits-model=./vits-icefall-zh-aishell3/model.onnx \ + --vits-lexicon=./vits-icefall-zh-aishell3/lexicon.txt \ + --vits-tokens=./vits-icefall-zh-aishell3/tokens.txt \ --sid=$sid \ --debug=1 \ --output-filename=./liliana-$sid.wav \ "林美丽最美丽、最漂亮、最可爱!" ./non-streaming-tts \ - --vits-model=./vits-zh-aishell3/vits-aishell3.onnx \ - --vits-lexicon=./vits-zh-aishell3/lexicon.txt \ - --vits-tokens=./vits-zh-aishell3/tokens.txt \ - --tts-rule-fsts=./vits-zh-aishell3/rule.fst \ + --vits-model=./vits-icefall-zh-aishell3/model.onnx \ + --vits-lexicon=./vits-icefall-zh-aishell3/lexicon.txt \ + --vits-tokens=./vits-icefall-zh-aishell3/tokens.txt \ + --tts-rule-fsts=./vits-icefall-zh-aishell3/phone.fst,./vits-icefall-zh-aishell3/date.fst,./vits-icefall-zh-aishell3/number.fst \ --sid=$sid \ --debug=1 \ --output-filename=./numbers-$sid.wav \ "数字12345.6789怎么念" + +./non-streaming-tts \ + --vits-model=./vits-icefall-zh-aishell3/model.onnx \ + --vits-lexicon=./vits-icefall-zh-aishell3/lexicon.txt \ + --vits-tokens=./vits-icefall-zh-aishell3/tokens.txt \ + --tts-rule-fsts=./vits-icefall-zh-aishell3/phone.fst,./vits-icefall-zh-aishell3/date.fst,./vits-icefall-zh-aishell3/number.fst \ + --tts-rule-fars=./vits-icefall-zh-aishell3/rule.far \ + --sid=$sid \ + --debug=1 \ + --output-filename=./heteronym-$sid.wav \ + "万古长存长沙长大长白山长孙长安街" done diff --git a/ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ViewModel.swift b/ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ViewModel.swift index f29de9e80..3e5c381c5 100644 --- a/ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ViewModel.swift +++ b/ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ViewModel.swift @@ -7,10 +7,9 @@ import Foundation - // used to get the path to espeak-ng-data func resourceURL(to path: String) -> String { - return URL(string: path, relativeTo: Bundle.main.resourceURL)!.path + return URL(string: path, relativeTo: Bundle.main.resourceURL)!.path } func getResource(_ forResource: String, _ ofType: String) -> String { @@ -50,8 +49,7 @@ func getTtsForAishell3() -> SherpaOnnxOfflineTtsWrapper { // See the following link // https://k2-fsa.github.io/sherpa/onnx/tts/pretrained_models/vits.html#vits-model-aishell3 - // vits-vctk.onnx - let model = getResource("vits-aishell3", "onnx") + let model = getResource("model", "onnx") // lexicon.txt let lexicon = getResource("lexicon", "txt") @@ -59,9 +57,19 @@ func getTtsForAishell3() -> SherpaOnnxOfflineTtsWrapper { // tokens.txt let tokens = getResource("tokens", "txt") + // rule.fst + let ruleFsts = getResource("rule", "fst") + + // rule.far + let ruleFars = getResource("rule", "far") + let vits = sherpaOnnxOfflineTtsVitsModelConfig(model: model, lexicon: lexicon, tokens: tokens) let modelConfig = sherpaOnnxOfflineTtsModelConfig(vits: vits) - var config = sherpaOnnxOfflineTtsConfig(model: modelConfig) + var config = sherpaOnnxOfflineTtsConfig( + model: modelConfig, + ruleFsts: ruleFsts, + ruleFars: ruleFars + ) return SherpaOnnxOfflineTtsWrapper(config: &config) } @@ -69,7 +77,6 @@ func getTtsForAishell3() -> SherpaOnnxOfflineTtsWrapper { func getTtsFor_en_US_amy_low() -> SherpaOnnxOfflineTtsWrapper { // please see https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-piper-en_US-amy-low.tar.bz2 - // vits-vctk.onnx let model = getResource("en_US-amy-low", "onnx") // tokens.txt @@ -78,7 +85,8 @@ func getTtsFor_en_US_amy_low() -> SherpaOnnxOfflineTtsWrapper { // in this case, we don't need lexicon.txt let dataDir = resourceURL(to: "espeak-ng-data") - let vits = sherpaOnnxOfflineTtsVitsModelConfig(model: model, lexicon: "", tokens: tokens, dataDir: dataDir) + let vits = sherpaOnnxOfflineTtsVitsModelConfig( + model: model, lexicon: "", tokens: tokens, dataDir: dataDir) let modelConfig = sherpaOnnxOfflineTtsModelConfig(vits: vits) var config = sherpaOnnxOfflineTtsConfig(model: modelConfig) diff --git a/mfc-examples/NonStreamingSpeechRecognition/sherpa-onnx-deps.props b/mfc-examples/NonStreamingSpeechRecognition/sherpa-onnx-deps.props index ae9e85186..e81f4b629 100644 --- a/mfc-examples/NonStreamingSpeechRecognition/sherpa-onnx-deps.props +++ b/mfc-examples/NonStreamingSpeechRecognition/sherpa-onnx-deps.props @@ -11,6 +11,7 @@ sherpa-onnx-core.lib; kaldi-decoder-core.lib; sherpa-onnx-kaldifst-core.lib; + sherpa-onnx-fstfar.lib; sherpa-onnx-fst.lib; kaldi-native-fbank-core.lib; onnxruntime.lib; diff --git a/mfc-examples/NonStreamingTextToSpeech/sherpa-onnx-deps.props b/mfc-examples/NonStreamingTextToSpeech/sherpa-onnx-deps.props index ae9e85186..e81f4b629 100644 --- a/mfc-examples/NonStreamingTextToSpeech/sherpa-onnx-deps.props +++ b/mfc-examples/NonStreamingTextToSpeech/sherpa-onnx-deps.props @@ -11,6 +11,7 @@ sherpa-onnx-core.lib; kaldi-decoder-core.lib; sherpa-onnx-kaldifst-core.lib; + sherpa-onnx-fstfar.lib; sherpa-onnx-fst.lib; kaldi-native-fbank-core.lib; onnxruntime.lib; diff --git a/mfc-examples/StreamingSpeechRecognition/sherpa-onnx-deps.props b/mfc-examples/StreamingSpeechRecognition/sherpa-onnx-deps.props index ae9e85186..e81f4b629 100644 --- a/mfc-examples/StreamingSpeechRecognition/sherpa-onnx-deps.props +++ b/mfc-examples/StreamingSpeechRecognition/sherpa-onnx-deps.props @@ -11,6 +11,7 @@ sherpa-onnx-core.lib; kaldi-decoder-core.lib; sherpa-onnx-kaldifst-core.lib; + sherpa-onnx-fstfar.lib; sherpa-onnx-fst.lib; kaldi-native-fbank-core.lib; onnxruntime.lib; diff --git a/nodejs-examples/README.md b/nodejs-examples/README.md index 9c13bee51..29c93a27d 100644 --- a/nodejs-examples/README.md +++ b/nodejs-examples/README.md @@ -43,8 +43,8 @@ for text-to-speech. You can use the following command to run it: ```bash -wget -q https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-zh-aishell3.tar.bz2 -tar xvf vits-zh-aishell3.tar.bz2 +wget -q https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-icefall-zh-aishell3.tar.bz2 +tar xvf vits-icefall-zh-aishell3.tar.bz2 node ./test-offline-tts-zh.js ``` diff --git a/nodejs-examples/test-offline-tts-en.js b/nodejs-examples/test-offline-tts-en.js index a87780175..c3bd67b43 100644 --- a/nodejs-examples/test-offline-tts-en.js +++ b/nodejs-examples/test-offline-tts-en.js @@ -22,6 +22,7 @@ function createOfflineTts() { let offlineTtsConfig = { offlineTtsModelConfig: offlineTtsModelConfig, ruleFsts: '', + ruleFars: '', maxNumSentences: 1, }; diff --git a/nodejs-examples/test-offline-tts-zh.js b/nodejs-examples/test-offline-tts-zh.js index bc808803d..a53748c77 100644 --- a/nodejs-examples/test-offline-tts-zh.js +++ b/nodejs-examples/test-offline-tts-zh.js @@ -4,9 +4,9 @@ const sherpa_onnx = require('sherpa-onnx'); function createOfflineTts() { let offlineTtsVitsModelConfig = { - model: './vits-zh-aishell3/vits-aishell3.onnx', - lexicon: './vits-zh-aishell3/lexicon.txt', - tokens: './vits-zh-aishell3/tokens.txt', + model: './vits-icefall-zh-aishell3/vits-aishell3.onnx', + lexicon: './vits-icefall-zh-aishell3/lexicon.txt', + tokens: './vits-icefall-zh-aishell3/tokens.txt', dataDir: '', noiseScale: 0.667, noiseScaleW: 0.8, @@ -21,7 +21,9 @@ function createOfflineTts() { let offlineTtsConfig = { offlineTtsModelConfig: offlineTtsModelConfig, - ruleFsts: './vits-zh-aishell3/rule.fst', + ruleFsts: + './vits-icefall-zh-aishell3/phone.fst,./vits-icefall-zh-aishell3/date.fst,./vits-icefall-zh-aishell3/number.fst,./vits-icefall-zh-aishell3/new_heteronym.fst', + ruleFars: './vits-icefall-zh-aishell3/rule.far', maxNumSentences: 1, }; diff --git a/scripts/apk/build-apk-tts-engine.sh.in b/scripts/apk/build-apk-tts-engine.sh.in index 5e46f8ebe..08d570384 100644 --- a/scripts/apk/build-apk-tts-engine.sh.in +++ b/scripts/apk/build-apk-tts-engine.sh.in @@ -56,6 +56,11 @@ sed -i.bak s/"lang = null"/"lang = \"$lang_iso_639_3\""/ ./TtsEngine.kt sed -i.bak s%"ruleFsts = null"%"ruleFsts = \"$rule_fsts\""% ./TtsEngine.kt {% endif %} +{% if tts_model.rule_fars %} + rule_fars={{ tts_model.rule_fars }} + sed -i.bak s%"ruleFsts = null"%"ruleFars = \"$rule_fars\""% ./TtsEngine.kt +{% endif %} + {% if tts_model.data_dir %} data_dir={{ tts_model.data_dir }} sed -i.bak s%"dataDir = null"%"dataDir = \"$data_dir\""% ./TtsEngine.kt diff --git a/scripts/apk/build-apk-tts.sh.in b/scripts/apk/build-apk-tts.sh.in index a3dd6b48d..5eb377e31 100644 --- a/scripts/apk/build-apk-tts.sh.in +++ b/scripts/apk/build-apk-tts.sh.in @@ -54,6 +54,11 @@ sed -i.bak s/"modelName = null"/"modelName = \"$model_name\""/ ./MainActivity.kt sed -i.bak s%"ruleFsts = null"%"ruleFsts = \"$rule_fsts\""% ./MainActivity.kt {% endif %} +{% if tts_model.rule_fars %} + rule_fars={{ tts_model.rule_fars }} + sed -i.bak s%"ruleFsts = null"%"ruleFars = \"$rule_fars\""% ./MainActivity.kt +{% endif %} + {% if tts_model.data_dir %} data_dir={{ tts_model.data_dir }} sed -i.bak s%"dataDir = null"%"dataDir = \"$data_dir\""% ./MainActivity.kt diff --git a/scripts/apk/generate-tts-apk-script.py b/scripts/apk/generate-tts-apk-script.py index 2c539d79e..1221c4d33 100755 --- a/scripts/apk/generate-tts-apk-script.py +++ b/scripts/apk/generate-tts-apk-script.py @@ -33,6 +33,7 @@ class TtsModel: model_name: str = "" lang: str = "" # en, zh, fr, de, etc. rule_fsts: Optional[List[str]] = None + rule_fars: Optional[List[str]] = None data_dir: Optional[str] = None is_char: bool = False lang_iso_639_3: str = "" @@ -241,98 +242,94 @@ def get_mimic3_models() -> List[TtsModel]: def get_vits_models() -> List[TtsModel]: - return [ + chinese_models = [ # Chinese TtsModel( model_dir="vits-icefall-zh-aishell3", model_name="model.onnx", lang="zh", - rule_fsts="vits-icefall-zh-aishell3/phone.fst,vits-icefall-zh-aishell3/date.fst,vits-icefall-zh-aishell3/rule.fst", + rule_fsts="vits-icefall-zh-aishell3/phone.fst,vits-icefall-zh-aishell3/date.fst,vits-icefall-zh-aishell3/number.fst,vits-icefall-zh-aishell3/new_heteronym.fst", + rule_fars="vits-icefall-zh-aishell3/rule.far", ), TtsModel( model_dir="vits-zh-aishell3", model_name="vits-aishell3.onnx", lang="zh", - rule_fsts="vits-zh-aishell3/rule.fst", ), TtsModel( model_dir="vits-zh-hf-doom", model_name="doom.onnx", lang="zh", - rule_fsts="vits-zh-hf-doom/rule.fst", ), TtsModel( model_dir="vits-zh-hf-echo", model_name="echo.onnx", lang="zh", - rule_fsts="vits-zh-hf-echo/rule.fst", ), TtsModel( model_dir="vits-zh-hf-zenyatta", model_name="zenyatta.onnx", lang="zh", - rule_fsts="vits-zh-hf-zenyatta/rule.fst", ), TtsModel( model_dir="vits-zh-hf-abyssinvoker", model_name="abyssinvoker.onnx", lang="zh", - rule_fsts="vits-zh-hf-abyssinvoker/rule.fst", ), TtsModel( model_dir="vits-zh-hf-keqing", model_name="keqing.onnx", lang="zh", - rule_fsts="vits-zh-hf-keqing/rule.fst", ), TtsModel( model_dir="vits-zh-hf-eula", model_name="eula.onnx", lang="zh", - rule_fsts="vits-zh-hf-eula/rule.fst", ), TtsModel( model_dir="vits-zh-hf-bronya", model_name="bronya.onnx", lang="zh", - rule_fsts="vits-zh-hf-bronya/rule.fst", ), TtsModel( model_dir="vits-zh-hf-theresa", model_name="theresa.onnx", lang="zh", - rule_fsts="vits-zh-hf-theresa/rule.fst", ), TtsModel( model_dir="vits-zh-hf-fanchen-wnj", model_name="vits-zh-hf-fanchen-wnj.onnx", lang="zh", - rule_fsts="vits-zh-hf-fanchen-wnj/rule.fst", ), TtsModel( model_dir="vits-zh-hf-fanchen-C", model_name="vits-zh-hf-fanchen-C.onnx", lang="zh", - rule_fsts="vits-zh-hf-fanchen-C/rule.fst", ), TtsModel( model_dir="vits-zh-hf-fanchen-ZhiHuiLaoZhe", model_name="vits-zh-hf-fanchen-ZhiHuiLaoZhe.onnx", lang="zh", - rule_fsts="vits-zh-hf-fanchen-ZhiHuiLaoZhe/rule.fst", ), TtsModel( model_dir="vits-zh-hf-fanchen-ZhiHuiLaoZhe_new", model_name="vits-zh-hf-fanchen-ZhiHuiLaoZhe_new.onnx", lang="zh", - rule_fsts="vits-zh-hf-fanchen-ZhiHuiLaoZhe_new/rule.fst", ), TtsModel( model_dir="vits-zh-hf-fanchen-unity", model_name="vits-zh-hf-fanchen-unity.onnx", lang="zh", - rule_fsts="vits-zh-hf-fanchen-unity/rule.fst", ), + ] + + rule_fsts = ["phone.fst", "date.fst", "number.fst", "new_heteronym.fst"] + for m in chinese_models: + s = [f"{m.model_dir}/{r}" for r in rule_fsts] + m.rule_fsts = ",".join(s) + m.rule_fars = f"{m.model_dir}/rule.far" + + all_models = chinese_models + [ TtsModel( model_dir="vits-cantonese-hf-xiaomaiiwn", model_name="vits-cantonese-hf-xiaomaiiwn.onnx", @@ -346,6 +343,8 @@ def get_vits_models() -> List[TtsModel]: # fmt: on ] + return all_models + def main(): args = get_args() diff --git a/scripts/dotnet/generate.py b/scripts/dotnet/generate.py index f24353f68..5268211b2 100755 --- a/scripts/dotnet/generate.py +++ b/scripts/dotnet/generate.py @@ -40,6 +40,7 @@ def process_linux(s): "libpiper_phonemize.so.1", "libsherpa-onnx-c-api.so", "libsherpa-onnx-core.so", + "libsherpa-onnx-fstfar.so.7", "libsherpa-onnx-fst.so.6", "libsherpa-onnx-kaldifst-core.so", "libucd.so", @@ -68,6 +69,7 @@ def process_macos(s): "libpiper_phonemize.1.dylib", "libsherpa-onnx-c-api.dylib", "libsherpa-onnx-core.dylib", + "libsherpa-onnx-fstfar.7.dylib", "libsherpa-onnx-fst.6.dylib", "libsherpa-onnx-kaldifst-core.dylib", "libucd.dylib", @@ -96,6 +98,7 @@ def process_windows(s, rid): "piper_phonemize.dll", "sherpa-onnx-c-api.dll", "sherpa-onnx-core.dll", + "sherpa-onnx-fstfar.lib", "sherpa-onnx-fst.lib", "sherpa-onnx-kaldifst-core.lib", "ucd.dll", diff --git a/scripts/dotnet/offline.cs b/scripts/dotnet/offline.cs index 1a8612f33..c885ca5b0 100644 --- a/scripts/dotnet/offline.cs +++ b/scripts/dotnet/offline.cs @@ -67,6 +67,7 @@ public OfflineTtsConfig() Model = new OfflineTtsModelConfig(); RuleFsts = ""; MaxNumSentences = 1; + RuleFars = ""; } public OfflineTtsModelConfig Model; @@ -74,6 +75,9 @@ public OfflineTtsConfig() public string RuleFsts; public int MaxNumSentences; + + [MarshalAs(UnmanagedType.LPStr)] + public string RuleFars; } public class OfflineTtsGeneratedAudio diff --git a/scripts/dotnet/run.sh b/scripts/dotnet/run.sh index d723a2d8d..5bd8627c3 100755 --- a/scripts/dotnet/run.sh +++ b/scripts/dotnet/run.sh @@ -41,6 +41,7 @@ if [ ! -f /tmp/linux/libsherpa-onnx-core.so ]; then cd .. rm -v libpiper_phonemize.so libpiper_phonemize.so.1.2.0 rm -v libsherpa-onnx-fst.so + rm -v libsherpa-onnx-fstfar.so rm -v libonnxruntime.so rm -v libcargs.so rm -rf wheel @@ -67,6 +68,7 @@ if [ ! -f /tmp/macos/libsherpa-onnx-core.dylib ]; then rm -v libonnxruntime.dylib rm -v libpiper_phonemize.1.2.0.dylib libpiper_phonemize.dylib rm -v libsherpa-onnx-fst.dylib + rm -v libsherpa-onnx-fstfar.dylib rm -rf wheel ls -lh cd .. diff --git a/scripts/go/_internal/build_darwin_amd64.go b/scripts/go/_internal/build_darwin_amd64.go index 577dfa955..29d1bd684 100644 --- a/scripts/go/_internal/build_darwin_amd64.go +++ b/scripts/go/_internal/build_darwin_amd64.go @@ -2,5 +2,5 @@ package sherpa_onnx -// #cgo LDFLAGS: -L ${SRCDIR}/lib/x86_64-apple-darwin -lsherpa-onnx-c-api -lsherpa-onnx-core -lkaldi-native-fbank-core -lkaldi-decoder-core -lsherpa-onnx-kaldifst-core -lsherpa-onnx-fst -lpiper_phonemize -lespeak-ng -lucd -lonnxruntime -Wl,-rpath,${SRCDIR}/lib/x86_64-apple-darwin +// #cgo LDFLAGS: -L ${SRCDIR}/lib/x86_64-apple-darwin -lsherpa-onnx-c-api -lsherpa-onnx-core -lkaldi-native-fbank-core -lkaldi-decoder-core -lsherpa-onnx-kaldifst-core -lsherpa-onnx-fstfar -lsherpa-onnx-fst -lpiper_phonemize -lespeak-ng -lucd -lonnxruntime -Wl,-rpath,${SRCDIR}/lib/x86_64-apple-darwin import "C" diff --git a/scripts/go/sherpa_onnx.go b/scripts/go/sherpa_onnx.go index 361d9775f..99ecd84d7 100644 --- a/scripts/go/sherpa_onnx.go +++ b/scripts/go/sherpa_onnx.go @@ -554,6 +554,7 @@ type OfflineTtsModelConfig struct { type OfflineTtsConfig struct { Model OfflineTtsModelConfig RuleFsts string + RuleFars string MaxNumSentences int } @@ -583,6 +584,9 @@ func NewOfflineTts(config *OfflineTtsConfig) *OfflineTts { c.rule_fsts = C.CString(config.RuleFsts) defer C.free(unsafe.Pointer(c.rule_fsts)) + c.rule_fars = C.CString(config.RuleFars) + defer C.free(unsafe.Pointer(c.rule_fars)) + c.max_num_sentences = C.int(config.MaxNumSentences) c.model.vits.model = C.CString(config.Model.Vits.Model) diff --git a/sherpa-onnx/c-api/c-api.cc b/sherpa-onnx/c-api/c-api.cc index 9292687af..c349dd3f2 100644 --- a/sherpa-onnx/c-api/c-api.cc +++ b/sherpa-onnx/c-api/c-api.cc @@ -818,6 +818,7 @@ SherpaOnnxOfflineTts *SherpaOnnxCreateOfflineTts( tts_config.model.debug = config->model.debug; tts_config.model.provider = SHERPA_ONNX_OR(config->model.provider, "cpu"); tts_config.rule_fsts = SHERPA_ONNX_OR(config->rule_fsts, ""); + tts_config.rule_fars = SHERPA_ONNX_OR(config->rule_fars, ""); tts_config.max_num_sentences = SHERPA_ONNX_OR(config->max_num_sentences, 2); if (tts_config.model.debug) { diff --git a/sherpa-onnx/c-api/c-api.h b/sherpa-onnx/c-api/c-api.h index 78641f9bf..276b35900 100644 --- a/sherpa-onnx/c-api/c-api.h +++ b/sherpa-onnx/c-api/c-api.h @@ -783,6 +783,7 @@ SHERPA_ONNX_API typedef struct SherpaOnnxOfflineTtsConfig { SherpaOnnxOfflineTtsModelConfig model; const char *rule_fsts; int32_t max_num_sentences; + const char *rule_fars; } SherpaOnnxOfflineTtsConfig; SHERPA_ONNX_API typedef struct SherpaOnnxGeneratedAudio { diff --git a/sherpa-onnx/csrc/CMakeLists.txt b/sherpa-onnx/csrc/CMakeLists.txt index 1ebdc6264..423a777f7 100644 --- a/sherpa-onnx/csrc/CMakeLists.txt +++ b/sherpa-onnx/csrc/CMakeLists.txt @@ -164,6 +164,7 @@ endif() if(SHERPA_ONNX_ENABLE_TTS) target_link_libraries(sherpa-onnx-core piper_phonemize) + target_link_libraries(sherpa-onnx-core fstfar fst) endif() if(SHERPA_ONNX_ENABLE_CHECK) diff --git a/sherpa-onnx/csrc/lexicon.cc b/sherpa-onnx/csrc/lexicon.cc index 14c3d37a2..e3a87eba3 100644 --- a/sherpa-onnx/csrc/lexicon.cc +++ b/sherpa-onnx/csrc/lexicon.cc @@ -18,7 +18,6 @@ #endif #include -#include // NOLINT #include "sherpa-onnx/csrc/macros.h" #include "sherpa-onnx/csrc/onnx-utils.h" @@ -26,6 +25,55 @@ namespace sherpa_onnx { +static std::vector ProcessHeteronyms( + const std::vector &words) { + std::vector ans; + ans.reserve(words.size()); + + int32_t num_words = static_cast(words.size()); + int32_t i = 0; + int32_t prev = -1; + while (i < num_words) { + // start of a phrase #$| + if ((i + 2 < num_words) && words[i] == "#" && words[i + 1] == "$" && + words[i + 2] == "|") { + if (prev == -1) { + prev = i + 3; + } + i = i + 3; + continue; + } + + // end of a phrase |$# + if ((i + 2 < num_words) && words[i] == "|" && words[i + 1] == "$" && + words[i + 2] == "#") { + if (prev != -1) { + std::ostringstream os; + for (int32_t k = prev; k < i; ++k) { + if (words[k] != "|" && words[k] != "$" && words[k] != "#") { + os << words[k]; + } + } + ans.push_back(os.str()); + + prev = -1; + } + + i += 3; + continue; + } + + if (prev == -1) { + // not inside a phrase + ans.push_back(words[i]); + } + + ++i; + } + + return ans; +} + static void ToLowerCase(std::string *in_out) { std::transform(in_out->begin(), in_out->end(), in_out->begin(), [](unsigned char c) { return std::tolower(c); }); @@ -148,36 +196,9 @@ std::vector> Lexicon::ConvertTextToTokenIdsChinese( const std::string &_text) const { std::string text(_text); ToLowerCase(&text); - std::vector words; - if (pattern_) { - // Handle polyphones - size_t pos = 0; - auto begin = std::sregex_iterator(text.begin(), text.end(), *pattern_); - auto end = std::sregex_iterator(); - for (std::sregex_iterator i = begin; i != end; ++i) { - std::smatch match = *i; - if (pos < match.position()) { - auto this_segment = text.substr(pos, match.position() - pos); - auto this_segment_words = SplitUtf8(this_segment); - words.insert(words.end(), this_segment_words.begin(), - this_segment_words.end()); - pos = match.position() + match.length(); - } else if (pos == match.position()) { - pos = match.position() + match.length(); - } - words.push_back(match.str()); - } - - if (pos < text.size()) { - auto this_segment = text.substr(pos, text.size() - pos); - auto this_segment_words = SplitUtf8(this_segment); - words.insert(words.end(), this_segment_words.begin(), - this_segment_words.end()); - } - } else { - words = SplitUtf8(text); - } + std::vector words = SplitUtf8(text); + words = ProcessHeteronyms(words); if (debug_) { fprintf(stderr, "Input text in string: %s\n", text.c_str()); @@ -357,9 +378,6 @@ void Lexicon::InitLexicon(std::istream &is) { std::string line; std::string phone; - std::ostringstream os; - std::string sep; - while (std::getline(is, line)) { std::istringstream iss(line); @@ -381,18 +399,9 @@ void Lexicon::InitLexicon(std::istream &is) { if (ids.empty()) { continue; } - if (language_ == Language::kChinese && word.size() > 3) { - // this is not a single word; - os << sep << word; - sep = "|"; - } word2ids_.insert({std::move(word), std::move(ids)}); } - - if (!sep.empty()) { - pattern_ = std::make_unique(os.str()); - } } void Lexicon::InitPunctuations(const std::string &punctuations) { diff --git a/sherpa-onnx/csrc/lexicon.h b/sherpa-onnx/csrc/lexicon.h index 97b0ff7ba..e26a2decc 100644 --- a/sherpa-onnx/csrc/lexicon.h +++ b/sherpa-onnx/csrc/lexicon.h @@ -7,7 +7,6 @@ #include #include -#include // NOLINT #include #include #include @@ -65,9 +64,6 @@ class Lexicon : public OfflineTtsFrontend { std::unordered_map token2id_; Language language_; bool debug_; - - // for Chinese polyphones - std::unique_ptr pattern_; }; } // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/offline-tts-vits-impl.h b/sherpa-onnx/csrc/offline-tts-vits-impl.h index 6bcfc0cac..f7ddd8e47 100644 --- a/sherpa-onnx/csrc/offline-tts-vits-impl.h +++ b/sherpa-onnx/csrc/offline-tts-vits-impl.h @@ -15,6 +15,9 @@ #include "android/asset_manager.h" #include "android/asset_manager_jni.h" #endif + +#include "fst/extensions/far/far.h" +#include "kaldifst/csrc/kaldi-fst-io.h" #include "kaldifst/csrc/text-normalizer.h" #include "sherpa-onnx/csrc/lexicon.h" #include "sherpa-onnx/csrc/macros.h" @@ -46,6 +49,32 @@ class OfflineTtsVitsImpl : public OfflineTtsImpl { tn_list_.push_back(std::make_unique(f)); } } + + if (!config.rule_fars.empty()) { + if (config.model.debug) { + SHERPA_ONNX_LOGE("Loading FST archives"); + } + std::vector files; + SplitStringToVector(config.rule_fars, ",", false, &files); + for (const auto &f : files) { + if (config.model.debug) { + SHERPA_ONNX_LOGE("rule far: %s", f.c_str()); + } + std::unique_ptr> reader( + fst::FarReader::Open(f)); + for (; !reader->Done(); reader->Next()) { + std::unique_ptr r( + fst::CastOrConvertToConstFst(reader->GetFst()->Copy())); + + tn_list_.push_back( + std::make_unique(std::move(r))); + } + } + + if (config.model.debug) { + SHERPA_ONNX_LOGE("FST archives loaded!"); + } + } } #if __ANDROID_API__ >= 9 diff --git a/sherpa-onnx/csrc/offline-tts.cc b/sherpa-onnx/csrc/offline-tts.cc index 6f7e472a2..34d4a39ca 100644 --- a/sherpa-onnx/csrc/offline-tts.cc +++ b/sherpa-onnx/csrc/offline-tts.cc @@ -20,7 +20,14 @@ void OfflineTtsConfig::Register(ParseOptions *po) { "It not empty, it contains a list of rule FST filenames." "Multiple filenames are separated by a comma and they are " "applied from left to right. An example value: " - "rule1.fst,rule2,fst,rule3.fst"); + "rule1.fst,rule2.fst,rule3.fst"); + + po->Register("tts-rule-fars", &rule_fars, + "It not empty, it contains a list of rule FST archive filenames." + "Multiple filenames are separated by a comma and they are " + "applied from left to right. An example value: " + "rule1.far,rule2.far,rule3.far. Note that an *.far can contain " + "multiple *.fst files"); po->Register( "tts-max-num-sentences", &max_num_sentences, @@ -41,6 +48,17 @@ bool OfflineTtsConfig::Validate() const { } } + if (!rule_fars.empty()) { + std::vector files; + SplitStringToVector(rule_fars, ",", false, &files); + for (const auto &f : files) { + if (!FileExists(f)) { + SHERPA_ONNX_LOGE("Rule far %s does not exist. ", f.c_str()); + return false; + } + } + } + return model.Validate(); } @@ -50,6 +68,7 @@ std::string OfflineTtsConfig::ToString() const { os << "OfflineTtsConfig("; os << "model=" << model.ToString() << ", "; os << "rule_fsts=\"" << rule_fsts << "\", "; + os << "rule_fars=\"" << rule_fars << "\", "; os << "max_num_sentences=" << max_num_sentences << ")"; return os.str(); diff --git a/sherpa-onnx/csrc/offline-tts.h b/sherpa-onnx/csrc/offline-tts.h index 354057bf1..0f4cd1211 100644 --- a/sherpa-onnx/csrc/offline-tts.h +++ b/sherpa-onnx/csrc/offline-tts.h @@ -29,6 +29,9 @@ struct OfflineTtsConfig { // If there are multiple rules, they are applied from left to right. std::string rule_fsts; + // If there are multiple FST archives, they are applied from left to right. + std::string rule_fars; + // Maximum number of sentences that we process at a time. // This is to avoid OOM for very long input text. // If you set it to -1, then we process all sentences in a single batch. @@ -36,9 +39,11 @@ struct OfflineTtsConfig { OfflineTtsConfig() = default; OfflineTtsConfig(const OfflineTtsModelConfig &model, - const std::string &rule_fsts, int32_t max_num_sentences) + const std::string &rule_fsts, const std::string &rule_fars, + int32_t max_num_sentences) : model(model), rule_fsts(rule_fsts), + rule_fars(rule_fars), max_num_sentences(max_num_sentences) {} void Register(ParseOptions *po); diff --git a/sherpa-onnx/jni/jni.cc b/sherpa-onnx/jni/jni.cc index 281fd4ee5..23596e976 100644 --- a/sherpa-onnx/jni/jni.cc +++ b/sherpa-onnx/jni/jni.cc @@ -878,6 +878,13 @@ static OfflineTtsConfig GetOfflineTtsConfig(JNIEnv *env, jobject config) { ans.rule_fsts = p; env->ReleaseStringUTFChars(s, p); + // for ruleFars + fid = env->GetFieldID(cls, "ruleFars", "Ljava/lang/String;"); + s = (jstring)env->GetObjectField(config, fid); + p = env->GetStringUTFChars(s, nullptr); + ans.rule_fars = p; + env->ReleaseStringUTFChars(s, p); + fid = env->GetFieldID(cls, "maxNumSentences", "I"); ans.max_num_sentences = env->GetIntField(config, fid); diff --git a/sherpa-onnx/python/csrc/offline-tts.cc b/sherpa-onnx/python/csrc/offline-tts.cc index ff31ded9b..dad330927 100644 --- a/sherpa-onnx/python/csrc/offline-tts.cc +++ b/sherpa-onnx/python/csrc/offline-tts.cc @@ -32,11 +32,12 @@ static void PybindOfflineTtsConfig(py::module *m) { py::class_(*m, "OfflineTtsConfig") .def(py::init<>()) .def(py::init(), + const std::string &, int32_t>(), py::arg("model"), py::arg("rule_fsts") = "", - py::arg("max_num_sentences") = 2) + py::arg("rule_fars") = "", py::arg("max_num_sentences") = 2) .def_readwrite("model", &PyClass::model) .def_readwrite("rule_fsts", &PyClass::rule_fsts) + .def_readwrite("rule_fars", &PyClass::rule_fars) .def_readwrite("max_num_sentences", &PyClass::max_num_sentences) .def("validate", &PyClass::Validate) .def("__str__", &PyClass::ToString); diff --git a/swift-api-examples/SherpaOnnx.swift b/swift-api-examples/SherpaOnnx.swift index b463c8667..69d97785d 100644 --- a/swift-api-examples/SherpaOnnx.swift +++ b/swift-api-examples/SherpaOnnx.swift @@ -652,12 +652,14 @@ func sherpaOnnxOfflineTtsModelConfig( func sherpaOnnxOfflineTtsConfig( model: SherpaOnnxOfflineTtsModelConfig, ruleFsts: String = "", + ruleFars: String = "", maxNumSenetences: Int = 2 ) -> SherpaOnnxOfflineTtsConfig { return SherpaOnnxOfflineTtsConfig( model: model, rule_fsts: toCPointer(ruleFsts), - max_num_sentences: Int32(maxNumSenetences) + max_num_sentences: Int32(maxNumSenetences), + rule_fars: toCPointer(ruleFars) ) } diff --git a/wasm/tts/sherpa-onnx-tts.js b/wasm/tts/sherpa-onnx-tts.js index c291d8a48..030177840 100644 --- a/wasm/tts/sherpa-onnx-tts.js +++ b/wasm/tts/sherpa-onnx-tts.js @@ -90,7 +90,7 @@ function initSherpaOnnxOfflineTtsModelConfig(config, Module) { function initSherpaOnnxOfflineTtsConfig(config, Module) { const modelConfig = initSherpaOnnxOfflineTtsModelConfig(config.offlineTtsModelConfig, Module); - const len = modelConfig.len + 2 * 4; + const len = modelConfig.len + 3 * 4; const ptr = Module._malloc(len); let offset = 0; @@ -98,12 +98,19 @@ function initSherpaOnnxOfflineTtsConfig(config, Module) { offset += modelConfig.len; const ruleFstsLen = Module.lengthBytesUTF8(config.ruleFsts) + 1; - const buffer = Module._malloc(ruleFstsLen); + const ruleFarsLen = Module.lengthBytesUTF8(config.ruleFars) + 1; + + const buffer = Module._malloc(ruleFstsLen + ruleFarsLen); Module.stringToUTF8(config.ruleFsts, buffer, ruleFstsLen); + Module.stringToUTF8(config.ruleFars, buffer + ruleFstsLen, ruleFarsLen); + Module.setValue(ptr + offset, buffer, 'i8*'); offset += 4; Module.setValue(ptr + offset, config.maxNumSentences, 'i32'); + offset += 4; + + Module.setValue(ptr + offset, buffer + ruleFstsLen, 'i8*'); return { buffer: buffer, ptr: ptr, len: len, config: modelConfig, @@ -190,6 +197,7 @@ function createOfflineTts(Module, myConfig) { let offlineTtsConfig = { offlineTtsModelConfig: offlineTtsModelConfig, ruleFsts: '', + ruleFars: '', maxNumSentences: 1, } diff --git a/wasm/tts/sherpa-onnx-wasm-main-tts.cc b/wasm/tts/sherpa-onnx-wasm-main-tts.cc index 71701419c..83090dc72 100644 --- a/wasm/tts/sherpa-onnx-wasm-main-tts.cc +++ b/wasm/tts/sherpa-onnx-wasm-main-tts.cc @@ -18,7 +18,7 @@ static_assert(sizeof(SherpaOnnxOfflineTtsModelConfig) == sizeof(SherpaOnnxOfflineTtsVitsModelConfig) + 3 * 4, ""); static_assert(sizeof(SherpaOnnxOfflineTtsConfig) == - sizeof(SherpaOnnxOfflineTtsModelConfig) + 2 * 4, + sizeof(SherpaOnnxOfflineTtsModelConfig) + 3 * 4, ""); void MyPrint(SherpaOnnxOfflineTtsConfig *tts_config) { @@ -40,6 +40,7 @@ void MyPrint(SherpaOnnxOfflineTtsConfig *tts_config) { fprintf(stdout, "----------tts config----------\n"); fprintf(stdout, "rule_fsts: %s\n", tts_config->rule_fsts); + fprintf(stdout, "rule_fars: %s\n", tts_config->rule_fars); fprintf(stdout, "max num sentences: %d\n", tts_config->max_num_sentences); } From 6fb8ceda57ef2a7cf5f7a7ec745c9acc02840374 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Mon, 8 Apr 2024 16:41:01 +0800 Subject: [PATCH 06/45] Add VAD examples using ALSA for recording (#739) --- .github/scripts/test-nodejs-npm.sh | 5 +- .github/workflows/build-wheels-aarch64.yaml | 21 ++- .../com/k2fsa/sherpa/onnx/MainActivity.kt | 2 +- cmake/cmake_extension.py | 1 + cmake/openfst.cmake | 4 + python-api-examples/vad-alsa.py | 107 ++++++++++++++ python-api-examples/vad-microphone.py | 125 ++++++++++++++++ .../vad-remove-non-speech-segments-alsa.py | 138 ++++++++++++++++++ .../vad-remove-non-speech-segments.py | 7 +- sherpa-onnx/csrc/CMakeLists.txt | 4 +- sherpa-onnx/csrc/sherpa-onnx-vad-alsa.cc | 132 +++++++++++++++++ .../csrc/sherpa-onnx-vad-microphone.cc | 16 +- sherpa-onnx/python/csrc/CMakeLists.txt | 1 + sherpa-onnx/python/csrc/sherpa-onnx.cc | 3 + sherpa-onnx/python/csrc/wave-writer.cc | 27 ++++ sherpa-onnx/python/csrc/wave-writer.h | 16 ++ sherpa-onnx/python/sherpa_onnx/__init__.py | 1 + 17 files changed, 601 insertions(+), 9 deletions(-) create mode 100755 python-api-examples/vad-alsa.py create mode 100755 python-api-examples/vad-microphone.py create mode 100755 python-api-examples/vad-remove-non-speech-segments-alsa.py create mode 100644 sherpa-onnx/csrc/sherpa-onnx-vad-alsa.cc create mode 100644 sherpa-onnx/python/csrc/wave-writer.cc create mode 100644 sherpa-onnx/python/csrc/wave-writer.h diff --git a/.github/scripts/test-nodejs-npm.sh b/.github/scripts/test-nodejs-npm.sh index 95dcf0271..a27214383 100755 --- a/.github/scripts/test-nodejs-npm.sh +++ b/.github/scripts/test-nodejs-npm.sh @@ -58,7 +58,6 @@ rm sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13.tar.bz2 node ./test-online-zipformer2-ctc.js rm -rf sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13 - curl -LS -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 tar xvf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 rm sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 @@ -70,9 +69,9 @@ rm -rf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18 curl -LS -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-piper-en_US-amy-low.tar.bz2 tar xf vits-piper-en_US-amy-low.tar.bz2 node ./test-offline-tts-en.js -rm vits-piper-en_US-amy-low* +rm -rf vits-piper-en_US-amy-low* curl -LS -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-icefall-zh-aishell3.tar.bz2 tar xvf vits-icefall-zh-aishell3.tar.bz2 node ./test-offline-tts-zh.js -rm vits-icefall-zh-aishell3* +rm -rf vits-icefall-zh-aishell3* diff --git a/.github/workflows/build-wheels-aarch64.yaml b/.github/workflows/build-wheels-aarch64.yaml index 17ff53d7e..4ecc7a415 100644 --- a/.github/workflows/build-wheels-aarch64.yaml +++ b/.github/workflows/build-wheels-aarch64.yaml @@ -59,8 +59,27 @@ jobs: run: | ls -lh ./wheelhouse/ + - name: Install patchelf + if: matrix.os == 'ubuntu-latest' + shell: bash + run: | + sudo apt-get update -q + sudo apt-get install -q -y patchelf + patchelf --help + + - name: Patch wheels + shell: bash + if: matrix.os == 'ubuntu-latest' + run: | + mkdir ./wheels + sudo ./scripts/wheel/patch_wheel.py --in-dir ./wheelhouse --out-dir ./wheels + + ls -lh ./wheels/ + rm -rf ./wheelhouse + mv ./wheels ./wheelhouse + - name: Publish to huggingface - if: matrix.python-version == 'cp38' && matrix.manylinux == 'manylinux2014' + if: (matrix.python-version == 'cp38' || matrix.python-version == 'cp39' ) && matrix.manylinux == 'manylinux2014' env: HF_TOKEN: ${{ secrets.HF_TOKEN }} uses: nick-fields/retry@v3 diff --git a/android/SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt b/android/SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt index 9f8e6325f..369aaa8c5 100644 --- a/android/SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt +++ b/android/SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt @@ -186,7 +186,7 @@ class MainActivity : AppCompatActivity() { // https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-icefall-zh-aishell3.tar.bz2 // modelDir = "vits-icefall-zh-aishell3" // modelName = "model.onnx" - // ruleFsts = "vits-icefall-zh-aishell3/phone.fst,vits-icefall-zh-aishell3/date.fst,vits-icefall-zh-aishell3/number.fst," + // ruleFsts = "vits-icefall-zh-aishell3/phone.fst,vits-icefall-zh-aishell3/date.fst,vits-icefall-zh-aishell3/number.fst,vits-icefall-zh-aishell3/new_heteronym.fst" // ruleFars = "vits-icefall-zh-aishell3/rule.far" // lexicon = "lexicon.txt" diff --git a/cmake/cmake_extension.py b/cmake/cmake_extension.py index ea52bdd64..75b09a5c5 100644 --- a/cmake/cmake_extension.py +++ b/cmake/cmake_extension.py @@ -67,6 +67,7 @@ def get_binaries(): "sherpa-onnx-alsa-offline", "sherpa-onnx-alsa-offline-speaker-identification", "sherpa-onnx-offline-tts-play-alsa", + "sherpa-onnx-vad-alsa", ] if is_windows(): diff --git a/cmake/openfst.cmake b/cmake/openfst.cmake index 575ea8aed..cb0826a98 100644 --- a/cmake/openfst.cmake +++ b/cmake/openfst.cmake @@ -75,6 +75,10 @@ function(download_openfst) set_target_properties(fst PROPERTIES OUTPUT_NAME "sherpa-onnx-fst") set_target_properties(fstfar PROPERTIES OUTPUT_NAME "sherpa-onnx-fstfar") + if(LINUX) + target_compile_options(fst PUBLIC -Wno-missing-template-keyword) + endif() + target_include_directories(fst PUBLIC ${openfst_SOURCE_DIR}/src/include diff --git a/python-api-examples/vad-alsa.py b/python-api-examples/vad-alsa.py new file mode 100755 index 000000000..8f23d477e --- /dev/null +++ b/python-api-examples/vad-alsa.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 + +""" +This script works only on Linux. It uses ALSA for recording. +""" + +import argparse +from pathlib import Path + +import sherpa_onnx + + +def get_args(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--silero-vad-model", + type=str, + required=True, + help="Path to silero_vad.onnx", + ) + + parser.add_argument( + "--device-name", + type=str, + required=True, + help=""" +The device name specifies which microphone to use in case there are several +on your system. You can use + + arecord -l + +to find all available microphones on your computer. For instance, if it outputs + +**** List of CAPTURE Hardware Devices **** +card 3: UACDemoV10 [UACDemoV1.0], device 0: USB Audio [USB Audio] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + +and if you want to select card 3 and the device 0 on that card, please use: + + plughw:3,0 + +as the device_name. + """, + ) + + return parser.parse_args() + + +def main(): + args = get_args() + if not Path(args.silero_vad_model).is_file(): + raise RuntimeError( + f"{args.silero_vad_model} does not exist. Please download it from " + "https://github.com/snakers4/silero-vad/blob/master/files/silero_vad.onnx" + ) + + device_name = args.device_name + print(f"device_name: {device_name}") + alsa = sherpa_onnx.Alsa(device_name) + + sample_rate = 16000 + samples_per_read = int(0.1 * sample_rate) # 0.1 second = 100 ms + + config = sherpa_onnx.VadModelConfig() + config.silero_vad.model = args.silero_vad_model + config.sample_rate = sample_rate + + vad = sherpa_onnx.VoiceActivityDetector(config, buffer_size_in_seconds=30) + + print("Started! Please speak. Press Ctrl C to exit") + + printed = False + k = 0 + try: + while True: + samples = alsa.read(samples_per_read) # a blocking read + + vad.accept_waveform(samples) + + if vad.is_speech_detected() and not printed: + print("Detected speech") + printed = True + + if not vad.is_speech_detected(): + printed = False + + while not vad.empty(): + samples = vad.front.samples + duration = len(samples) / sample_rate + filename = f"seg-{k}-{duration:.3f}-seconds.wav" + k += 1 + sherpa_onnx.write_wave(filename, samples, sample_rate) + print(f"Duration: {duration:.3f} seconds") + print(f"Saved to {filename}") + print("----------") + + vad.pop() + except KeyboardInterrupt: + print("\nCaught Ctrl + C. Exit") + + +if __name__ == "__main__": + main() diff --git a/python-api-examples/vad-microphone.py b/python-api-examples/vad-microphone.py new file mode 100755 index 000000000..85cde0830 --- /dev/null +++ b/python-api-examples/vad-microphone.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 + +import argparse +import os +import sys +from pathlib import Path + +try: + import sounddevice as sd +except ImportError: + print("Please install sounddevice first. You can use") + print() + print(" pip install sounddevice") + print() + print("to install it") + sys.exit(-1) + +import sherpa_onnx + + +def get_args(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--silero-vad-model", + type=str, + required=True, + help="Path to silero_vad.onnx", + ) + + return parser.parse_args() + + +def main(): + args = get_args() + if not Path(args.silero_vad_model).is_file(): + raise RuntimeError( + f"{args.silero_vad_model} does not exist. Please download it from " + "https://github.com/snakers4/silero-vad/blob/master/files/silero_vad.onnx" + ) + + mic_sample_rate = 16000 + if "SHERPA_ONNX_MIC_SAMPLE_RATE" in os.environ: + mic_sample_rate = int(os.environ.get("SHERPA_ONNX_MIC_SAMPLE_RATE")) + print(f"Change microphone sample rate to {mic_sample_rate}") + + sample_rate = 16000 + samples_per_read = int(0.1 * sample_rate) # 0.1 second = 100 ms + + config = sherpa_onnx.VadModelConfig() + config.silero_vad.model = args.silero_vad_model + config.sample_rate = sample_rate + + vad = sherpa_onnx.VoiceActivityDetector(config, buffer_size_in_seconds=30) + + # python3 -m sounddevice + # can also be used to list all devices + + devices = sd.query_devices() + if len(devices) == 0: + print("No microphone devices found") + print( + "If you are using Linux and you are sure there is a microphone " + "on your system, please use " + "./vad-alsa.py" + ) + sys.exit(0) + + print(devices) + + if "SHERPA_ONNX_MIC_DEVICE" in os.environ: + input_device_idx = int(os.environ.get("SHERPA_ONNX_MIC_DEVICE")) + sd.default.device[0] = input_device_idx + print(f'Use selected device: {devices[input_device_idx]["name"]}') + else: + input_device_idx = sd.default.device[0] + print(f'Use default device: {devices[input_device_idx]["name"]}') + + print("Started! Please speak. Press Ctrl C to exit") + + printed = False + k = 0 + try: + with sd.InputStream( + channels=1, dtype="float32", samplerate=mic_sample_rate + ) as s: + while True: + samples, _ = s.read(samples_per_read) # a blocking read + samples = samples.reshape(-1) + + if mic_sample_rate != sample_rate: + import librosa + + samples = librosa.resample( + samples, orig_sr=mic_sample_rate, target_sr=sample_rate + ) + + vad.accept_waveform(samples) + + if vad.is_speech_detected() and not printed: + print("Detected speech") + printed = True + + if not vad.is_speech_detected(): + printed = False + + while not vad.empty(): + samples = vad.front.samples + duration = len(samples) / sample_rate + filename = f"seg-{k}-{duration:.3f}-seconds.wav" + k += 1 + sherpa_onnx.write_wave(filename, samples, sample_rate) + print(f"Duration: {duration:.3f} seconds") + print(f"Saved to {filename}") + print("----------") + + vad.pop() + except KeyboardInterrupt: + print("\nCaught Ctrl + C. Exit") + + +if __name__ == "__main__": + main() diff --git a/python-api-examples/vad-remove-non-speech-segments-alsa.py b/python-api-examples/vad-remove-non-speech-segments-alsa.py new file mode 100755 index 000000000..34f88e40f --- /dev/null +++ b/python-api-examples/vad-remove-non-speech-segments-alsa.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 + +""" +This file shows how to remove non-speech segments +and merge all speech segments into a large segment +and save it to a file. + +Different from ./vad-remove-non-speech-segments.py, this file supports only +Linux. + +Usage + +python3 ./vad-remove-non-speech-segments-alsa.py \ + --silero-vad-model silero_vad.onnx + +Please visit +https://github.com/snakers4/silero-vad/blob/master/files/silero_vad.onnx +to download silero_vad.onnx + +For instance, + +wget https://github.com/snakers4/silero-vad/raw/master/files/silero_vad.onnx +""" + +import argparse +import time +from pathlib import Path + +import numpy as np +import sherpa_onnx +import soundfile as sf + + +def assert_file_exists(filename: str): + assert Path(filename).is_file(), ( + f"{filename} does not exist!\n" + "Please refer to " + "https://k2-fsa.github.io/sherpa/onnx/pretrained_models/index.html to download it" + ) + + +def get_args(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--silero-vad-model", + type=str, + required=True, + help="Path to silero_vad.onnx", + ) + + parser.add_argument( + "--device-name", + type=str, + required=True, + help=""" +The device name specifies which microphone to use in case there are several +on your system. You can use + + arecord -l + +to find all available microphones on your computer. For instance, if it outputs + +**** List of CAPTURE Hardware Devices **** +card 3: UACDemoV10 [UACDemoV1.0], device 0: USB Audio [USB Audio] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + +and if you want to select card 3 and the device 0 on that card, please use: + + plughw:3,0 + +as the device_name. + """, + ) + + return parser.parse_args() + + +def main(): + args = get_args() + assert_file_exists(args.silero_vad_model) + + device_name = args.device_name + print(f"device_name: {device_name}") + alsa = sherpa_onnx.Alsa(device_name) + + sample_rate = 16000 + samples_per_read = int(0.1 * sample_rate) # 0.1 second = 100 ms + + config = sherpa_onnx.VadModelConfig() + config.silero_vad.model = args.silero_vad_model + config.sample_rate = sample_rate + + window_size = config.silero_vad.window_size + + buffer = [] + vad = sherpa_onnx.VoiceActivityDetector(config, buffer_size_in_seconds=30) + + all_samples = [] + + print("Started! Please speak. Press Ctrl C to exit") + + try: + while True: + samples = alsa.read(samples_per_read) # a blocking read + samples = np.array(samples) + + buffer = np.concatenate([buffer, samples]) + + all_samples = np.concatenate([all_samples, samples]) + + while len(buffer) > window_size: + vad.accept_waveform(buffer[:window_size]) + buffer = buffer[window_size:] + except KeyboardInterrupt: + print("\nCaught Ctrl + C. Saving & Exiting") + + speech_samples = [] + while not vad.empty(): + speech_samples.extend(vad.front.samples) + vad.pop() + + speech_samples = np.array(speech_samples, dtype=np.float32) + + filename_for_speech = time.strftime("%Y%m%d-%H%M%S-speech.wav") + sf.write(filename_for_speech, speech_samples, samplerate=sample_rate) + + filename_for_all = time.strftime("%Y%m%d-%H%M%S-all.wav") + sf.write(filename_for_all, all_samples, samplerate=sample_rate) + + print(f"Saved to {filename_for_speech} and {filename_for_all}") + + +if __name__ == "__main__": + main() diff --git a/python-api-examples/vad-remove-non-speech-segments.py b/python-api-examples/vad-remove-non-speech-segments.py index e55d88b07..e242801aa 100755 --- a/python-api-examples/vad-remove-non-speech-segments.py +++ b/python-api-examples/vad-remove-non-speech-segments.py @@ -66,6 +66,11 @@ def main(): devices = sd.query_devices() if len(devices) == 0: print("No microphone devices found") + print( + "If you are using Linux and you are sure there is a microphone " + "on your system, please use " + "./vad-remove-non-speech-segments-alsa.py" + ) sys.exit(0) print(devices) @@ -89,7 +94,7 @@ def main(): all_samples = [] - print("Started! Please speak") + print("Started! Please speak. Press Ctrl C to exit") try: with sd.InputStream(channels=1, dtype="float32", samplerate=sample_rate) as s: diff --git a/sherpa-onnx/csrc/CMakeLists.txt b/sherpa-onnx/csrc/CMakeLists.txt index 423a777f7..bedd1ed2a 100644 --- a/sherpa-onnx/csrc/CMakeLists.txt +++ b/sherpa-onnx/csrc/CMakeLists.txt @@ -251,6 +251,7 @@ if(SHERPA_ONNX_HAS_ALSA AND SHERPA_ONNX_ENABLE_BINARY) add_executable(sherpa-onnx-keyword-spotter-alsa sherpa-onnx-keyword-spotter-alsa.cc alsa.cc) add_executable(sherpa-onnx-alsa-offline sherpa-onnx-alsa-offline.cc alsa.cc) add_executable(sherpa-onnx-alsa-offline-speaker-identification sherpa-onnx-alsa-offline-speaker-identification.cc alsa.cc) + add_executable(sherpa-onnx-vad-alsa sherpa-onnx-vad-alsa.cc alsa.cc) if(SHERPA_ONNX_ENABLE_TTS) @@ -259,9 +260,10 @@ if(SHERPA_ONNX_HAS_ALSA AND SHERPA_ONNX_ENABLE_BINARY) set(exes sherpa-onnx-alsa - sherpa-onnx-keyword-spotter-alsa sherpa-onnx-alsa-offline sherpa-onnx-alsa-offline-speaker-identification + sherpa-onnx-keyword-spotter-alsa + sherpa-onnx-vad-alsa ) if(SHERPA_ONNX_ENABLE_TTS) diff --git a/sherpa-onnx/csrc/sherpa-onnx-vad-alsa.cc b/sherpa-onnx/csrc/sherpa-onnx-vad-alsa.cc new file mode 100644 index 000000000..31a3f39b0 --- /dev/null +++ b/sherpa-onnx/csrc/sherpa-onnx-vad-alsa.cc @@ -0,0 +1,132 @@ +// sherpa-onnx/csrc/sherpa-onnx-vad-alsa.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include +#include +#include + +#include + +#include "sherpa-onnx/csrc/alsa.h" +#include "sherpa-onnx/csrc/circular-buffer.h" +#include "sherpa-onnx/csrc/voice-activity-detector.h" +#include "sherpa-onnx/csrc/wave-writer.h" + +bool stop = false; +static void Handler(int32_t sig) { + stop = true; + fprintf(stderr, "\nCaught Ctrl + C. Exiting...\n"); +} + +int32_t main(int32_t argc, char *argv[]) { + signal(SIGINT, Handler); + + const char *kUsageMessage = R"usage( +This program shows how to use VAD in sherpa-onnx. + + ./bin/sherpa-onnx-vad-alsa \ + --silero-vad-model=/path/to/silero_vad.onnx \ + device_name + +Please download silero_vad.onnx from +https://github.com/snakers4/silero-vad/blob/master/files/silero_vad.onnx + +For instance, use +wget https://github.com/snakers4/silero-vad/raw/master/files/silero_vad.onnx + +The device name specifies which microphone to use in case there are several +on your system. You can use + + arecord -l + +to find all available microphones on your computer. For instance, if it outputs + +**** List of CAPTURE Hardware Devices **** +card 3: UACDemoV10 [UACDemoV1.0], device 0: USB Audio [USB Audio] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + +and if you want to select card 3 and the device 0 on that card, please use: + + plughw:3,0 + +as the device_name. +)usage"; + + sherpa_onnx::ParseOptions po(kUsageMessage); + sherpa_onnx::VadModelConfig config; + + config.Register(&po); + po.Read(argc, argv); + if (po.NumArgs() != 1) { + fprintf(stderr, "Please provide only 1 argument: the device name\n"); + po.PrintUsage(); + exit(EXIT_FAILURE); + } + + fprintf(stderr, "%s\n", config.ToString().c_str()); + + if (!config.Validate()) { + fprintf(stderr, "Errors in config!\n"); + return -1; + } + + std::string device_name = po.GetArg(1); + sherpa_onnx::Alsa alsa(device_name.c_str()); + fprintf(stderr, "Use recording device: %s\n", device_name.c_str()); + + int32_t sample_rate = 16000; + + if (alsa.GetExpectedSampleRate() != sample_rate) { + fprintf(stderr, "sample rate: %d != %d\n", alsa.GetExpectedSampleRate(), + sample_rate); + exit(-1); + } + + int32_t chunk = 0.1 * alsa.GetActualSampleRate(); + + auto vad = std::make_unique(config); + + fprintf(stderr, "Started. Please speak\n"); + + int32_t window_size = config.silero_vad.window_size; + bool printed = false; + + int32_t k = 0; + while (!stop) { + { + const std::vector &samples = alsa.Read(chunk); + + vad->AcceptWaveform(samples.data(), samples.size()); + + if (vad->IsSpeechDetected() && !printed) { + printed = true; + fprintf(stderr, "\nDetected speech!\n"); + } + if (!vad->IsSpeechDetected()) { + printed = false; + } + + while (!vad->Empty()) { + const auto &segment = vad->Front(); + float duration = + segment.samples.size() / static_cast(sample_rate); + + fprintf(stderr, "Duration: %.3f seconds\n", duration); + + char filename[128]; + snprintf(filename, sizeof(filename), "seg-%d-%.3fs.wav", k, duration); + k += 1; + sherpa_onnx::WriteWave(filename, 16000, segment.samples.data(), + segment.samples.size()); + fprintf(stderr, "Saved to %s\n", filename); + fprintf(stderr, "----------\n"); + + vad->Pop(); + } + } + } + + return 0; +} diff --git a/sherpa-onnx/csrc/sherpa-onnx-vad-microphone.cc b/sherpa-onnx/csrc/sherpa-onnx-vad-microphone.cc index 19dd1d85f..da013b9e8 100644 --- a/sherpa-onnx/csrc/sherpa-onnx-vad-microphone.cc +++ b/sherpa-onnx/csrc/sherpa-onnx-vad-microphone.cc @@ -13,6 +13,7 @@ #include "sherpa-onnx/csrc/circular-buffer.h" #include "sherpa-onnx/csrc/microphone.h" #include "sherpa-onnx/csrc/voice-activity-detector.h" +#include "sherpa-onnx/csrc/wave-writer.h" bool stop = false; std::mutex mutex; @@ -122,6 +123,7 @@ wget https://github.com/snakers4/silero-vad/raw/master/files/silero_vad.onnx int32_t window_size = config.silero_vad.window_size; bool printed = false; + int32_t k = 0; while (!stop) { { std::lock_guard lock(mutex); @@ -140,9 +142,19 @@ wget https://github.com/snakers4/silero-vad/raw/master/files/silero_vad.onnx } while (!vad->Empty()) { - float duration = vad->Front().samples.size() / sample_rate; - vad->Pop(); + const auto &segment = vad->Front(); + float duration = segment.samples.size() / sample_rate; fprintf(stderr, "Duration: %.3f seconds\n", duration); + + char filename[128]; + snprintf(filename, sizeof(filename), "seg-%d-%.3fs.wav", k, duration); + k += 1; + sherpa_onnx::WriteWave(filename, 16000, segment.samples.data(), + segment.samples.size()); + fprintf(stderr, "Saved to %s\n", filename); + fprintf(stderr, "----------\n"); + + vad->Pop(); } } } diff --git a/sherpa-onnx/python/csrc/CMakeLists.txt b/sherpa-onnx/python/csrc/CMakeLists.txt index 53aebd78c..12409a9be 100644 --- a/sherpa-onnx/python/csrc/CMakeLists.txt +++ b/sherpa-onnx/python/csrc/CMakeLists.txt @@ -35,6 +35,7 @@ set(srcs vad-model-config.cc vad-model.cc voice-activity-detector.cc + wave-writer.cc ) if(SHERPA_ONNX_HAS_ALSA) list(APPEND srcs ${CMAKE_SOURCE_DIR}/sherpa-onnx/csrc/alsa.cc alsa.cc) diff --git a/sherpa-onnx/python/csrc/sherpa-onnx.cc b/sherpa-onnx/python/csrc/sherpa-onnx.cc index 4952e150b..8a5ae5cd3 100644 --- a/sherpa-onnx/python/csrc/sherpa-onnx.cc +++ b/sherpa-onnx/python/csrc/sherpa-onnx.cc @@ -26,6 +26,7 @@ #include "sherpa-onnx/python/csrc/vad-model-config.h" #include "sherpa-onnx/python/csrc/vad-model.h" #include "sherpa-onnx/python/csrc/voice-activity-detector.h" +#include "sherpa-onnx/python/csrc/wave-writer.h" #if SHERPA_ONNX_ENABLE_TTS == 1 #include "sherpa-onnx/python/csrc/offline-tts.h" @@ -36,6 +37,8 @@ namespace sherpa_onnx { PYBIND11_MODULE(_sherpa_onnx, m) { m.doc() = "pybind11 binding of sherpa-onnx"; + PybindWaveWriter(&m); + PybindFeatures(&m); PybindOnlineCtcFstDecoderConfig(&m); PybindOnlineModelConfig(&m); diff --git a/sherpa-onnx/python/csrc/wave-writer.cc b/sherpa-onnx/python/csrc/wave-writer.cc new file mode 100644 index 000000000..6ec4d65df --- /dev/null +++ b/sherpa-onnx/python/csrc/wave-writer.cc @@ -0,0 +1,27 @@ +// sherpa-onnx/python/csrc/wave-writer.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/python/csrc/wave-writer.h" + +#include +#include + +#include "sherpa-onnx/csrc/wave-writer.h" + +namespace sherpa_onnx { + +void PybindWaveWriter(py::module *m) { + m->def( + "write_wave", + [](const std::string &filename, const std::vector &samples, + int32_t sample_rate) -> bool { + bool ok = + WriteWave(filename, sample_rate, samples.data(), samples.size()); + + return ok; + }, + py::arg("filename"), py::arg("samples"), py::arg("sample_rate")); +} + +} // namespace sherpa_onnx diff --git a/sherpa-onnx/python/csrc/wave-writer.h b/sherpa-onnx/python/csrc/wave-writer.h new file mode 100644 index 000000000..c8ab58d5b --- /dev/null +++ b/sherpa-onnx/python/csrc/wave-writer.h @@ -0,0 +1,16 @@ +// sherpa-onnx/python/csrc/wave-writer.h +// +// Copyright (c) 2024 Xiaomi Corporation + +#ifndef SHERPA_ONNX_PYTHON_CSRC_WAVE_WRITER_H_ +#define SHERPA_ONNX_PYTHON_CSRC_WAVE_WRITER_H_ + +#include "sherpa-onnx/python/csrc/sherpa-onnx.h" + +namespace sherpa_onnx { + +void PybindWaveWriter(py::module *m); + +} + +#endif // SHERPA_ONNX_PYTHON_CSRC_WAVE_WRITER_H_ diff --git a/sherpa-onnx/python/sherpa_onnx/__init__.py b/sherpa-onnx/python/sherpa_onnx/__init__.py index 1f98bef69..2282687ea 100644 --- a/sherpa-onnx/python/sherpa_onnx/__init__.py +++ b/sherpa-onnx/python/sherpa_onnx/__init__.py @@ -19,6 +19,7 @@ VadModel, VadModelConfig, VoiceActivityDetector, + write_wave, ) from .keyword_spotter import KeywordSpotter From 6b3d2b87f9a780e774400d5065a5d44b73c1a3eb Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Mon, 8 Apr 2024 17:22:48 +0800 Subject: [PATCH 07/45] Fix releasing GIL (#741) --- sherpa-onnx/python/csrc/offline-stream.cc | 13 ++----------- sherpa-onnx/python/csrc/online-stream.cc | 3 ++- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/sherpa-onnx/python/csrc/offline-stream.cc b/sherpa-onnx/python/csrc/offline-stream.cc index bd1d0650f..32c6e4149 100644 --- a/sherpa-onnx/python/csrc/offline-stream.cc +++ b/sherpa-onnx/python/csrc/offline-stream.cc @@ -53,17 +53,8 @@ void PybindOfflineStream(py::module *m) { py::class_(*m, "OfflineStream") .def( "accept_waveform", - [](PyClass &self, float sample_rate, py::array_t waveform) { -#if 0 - auto report_gil_status = []() { - auto is_gil_held = false; - if (auto tstate = py::detail::get_thread_state_unchecked()) - is_gil_held = (tstate == PyGILState_GetThisThreadState()); - - return is_gil_held ? "GIL held" : "GIL released"; - }; - std::cout << report_gil_status() << "\n"; -#endif + [](PyClass &self, float sample_rate, + const std::vector &waveform) { self.AcceptWaveform(sample_rate, waveform.data(), waveform.size()); }, py::arg("sample_rate"), py::arg("waveform"), kAcceptWaveformUsage, diff --git a/sherpa-onnx/python/csrc/online-stream.cc b/sherpa-onnx/python/csrc/online-stream.cc index 9f8a17b9c..208000657 100644 --- a/sherpa-onnx/python/csrc/online-stream.cc +++ b/sherpa-onnx/python/csrc/online-stream.cc @@ -25,7 +25,8 @@ void PybindOnlineStream(py::module *m) { py::class_(*m, "OnlineStream") .def( "accept_waveform", - [](PyClass &self, float sample_rate, py::array_t waveform) { + [](PyClass &self, float sample_rate, + const std::vector &waveform) { self.AcceptWaveform(sample_rate, waveform.data(), waveform.size()); }, py::arg("sample_rate"), py::arg("waveform"), kAcceptWaveformUsage, From 0d90b34e4add8fadcfaa879fc82a9fa4be698c6a Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Mon, 8 Apr 2024 21:36:47 +0800 Subject: [PATCH 08/45] Support Chinese heteronyms on Android for TTS. (#742) --- cmake/openfst.cmake | 16 ++++++------ scripts/dotnet/generate.py | 8 +++--- sherpa-onnx/csrc/offline-tts-vits-impl.h | 31 +++++++++++++++++++++++ sherpa-onnx/python/csrc/offline-stream.cc | 2 ++ sherpa-onnx/python/csrc/online-stream.cc | 2 ++ 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/cmake/openfst.cmake b/cmake/openfst.cmake index cb0826a98..b6633437f 100644 --- a/cmake/openfst.cmake +++ b/cmake/openfst.cmake @@ -3,18 +3,18 @@ function(download_openfst) include(FetchContent) - set(openfst_URL "https://github.com/kkm000/openfst/archive/refs/tags/win/1.6.5.1.tar.gz") - set(openfst_URL2 "https://hub.nuaa.cf/kkm000/openfst/archive/refs/tags/win/1.6.5.1.tar.gz") - set(openfst_HASH "SHA256=02c49b559c3976a536876063369efc0e41ab374be1035918036474343877046e") + set(openfst_URL "https://github.com/csukuangfj/openfst/archive/792965fda2a3bc29f282321f527af0d6ba26fd22.zip") + set(openfst_URL2 "https://hub.nuaa.cf/csukuangfj/openfst/archive/792965fda2a3bc29f282321f527af0d6ba26fd22.zip") + set(openfst_HASH "SHA256=815d8acf555e4aaece294d6280ec209d0e9d91e0120e8406b24ff7124ecdbb26") # If you don't have access to the Internet, # please pre-download it set(possible_file_locations - $ENV{HOME}/Downloads/openfst-win-1.6.5.1.tar.gz - ${CMAKE_SOURCE_DIR}/openfst-win-1.6.5.1.tar.gz - ${CMAKE_BINARY_DIR}/openfst-win-1.6.5.1.tar.gz - /tmp/openfst-win-1.6.5.1.tar.gz - /star-fj/fangjun/download/github/openfst-win-1.6.5.1.tar.gz + $ENV{HOME}/Downloads/openfst-792965fda2a3bc29f282321f527af0d6ba26fd22.zip + ${CMAKE_SOURCE_DIR}/openfst-792965fda2a3bc29f282321f527af0d6ba26fd22.zip + ${CMAKE_BINARY_DIR}/openfst-792965fda2a3bc29f282321f527af0d6ba26fd22.zip + /tmp/openfst-792965fda2a3bc29f282321f527af0d6ba26fd22.zip + /star-fj/fangjun/download/github/openfst-792965fda2a3bc29f282321f527af0d6ba26fd22.zip ) foreach(f IN LISTS possible_file_locations) diff --git a/scripts/dotnet/generate.py b/scripts/dotnet/generate.py index 5268211b2..ed395f149 100755 --- a/scripts/dotnet/generate.py +++ b/scripts/dotnet/generate.py @@ -40,8 +40,8 @@ def process_linux(s): "libpiper_phonemize.so.1", "libsherpa-onnx-c-api.so", "libsherpa-onnx-core.so", - "libsherpa-onnx-fstfar.so.7", - "libsherpa-onnx-fst.so.6", + "libsherpa-onnx-fstfar.so.16", + "libsherpa-onnx-fst.so.16", "libsherpa-onnx-kaldifst-core.so", "libucd.so", ] @@ -69,8 +69,8 @@ def process_macos(s): "libpiper_phonemize.1.dylib", "libsherpa-onnx-c-api.dylib", "libsherpa-onnx-core.dylib", - "libsherpa-onnx-fstfar.7.dylib", - "libsherpa-onnx-fst.6.dylib", + "libsherpa-onnx-fstfar.16.dylib", + "libsherpa-onnx-fst.16.dylib", "libsherpa-onnx-kaldifst-core.dylib", "libucd.dylib", ] diff --git a/sherpa-onnx/csrc/offline-tts-vits-impl.h b/sherpa-onnx/csrc/offline-tts-vits-impl.h index f7ddd8e47..a873fd8f6 100644 --- a/sherpa-onnx/csrc/offline-tts-vits-impl.h +++ b/sherpa-onnx/csrc/offline-tts-vits-impl.h @@ -56,6 +56,9 @@ class OfflineTtsVitsImpl : public OfflineTtsImpl { } std::vector files; SplitStringToVector(config.rule_fars, ",", false, &files); + + tn_list_.reserve(files.size() + tn_list_.size()); + for (const auto &f : files) { if (config.model.debug) { SHERPA_ONNX_LOGE("rule far: %s", f.c_str()); @@ -96,6 +99,34 @@ class OfflineTtsVitsImpl : public OfflineTtsImpl { tn_list_.push_back(std::make_unique(is)); } } + + if (!config.rule_fars.empty()) { + std::vector files; + SplitStringToVector(config.rule_fars, ",", false, &files); + tn_list_.reserve(files.size() + tn_list_.size()); + + for (const auto &f : files) { + if (config.model.debug) { + SHERPA_ONNX_LOGE("rule far: %s", f.c_str()); + } + + auto buf = ReadFile(mgr, f); + + std::unique_ptr s( + new std::istrstream(buf.data(), buf.size())); + + std::unique_ptr> reader( + fst::FarReader::Open(std::move(s))); + + for (; !reader->Done(); reader->Next()) { + std::unique_ptr r( + fst::CastOrConvertToConstFst(reader->GetFst()->Copy())); + + tn_list_.push_back( + std::make_unique(std::move(r))); + } // for (; !reader->Done(); reader->Next()) + } // for (const auto &f : files) + } // if (!config.rule_fars.empty()) } #endif diff --git a/sherpa-onnx/python/csrc/offline-stream.cc b/sherpa-onnx/python/csrc/offline-stream.cc index 32c6e4149..80d54d6cf 100644 --- a/sherpa-onnx/python/csrc/offline-stream.cc +++ b/sherpa-onnx/python/csrc/offline-stream.cc @@ -4,6 +4,8 @@ #include "sherpa-onnx/python/csrc/offline-stream.h" +#include + #include "sherpa-onnx/csrc/offline-stream.h" namespace sherpa_onnx { diff --git a/sherpa-onnx/python/csrc/online-stream.cc b/sherpa-onnx/python/csrc/online-stream.cc index 208000657..688a64f91 100644 --- a/sherpa-onnx/python/csrc/online-stream.cc +++ b/sherpa-onnx/python/csrc/online-stream.cc @@ -4,6 +4,8 @@ #include "sherpa-onnx/python/csrc/online-stream.h" +#include + #include "sherpa-onnx/csrc/online-stream.h" namespace sherpa_onnx { From db1b3ab1f384bfd77005ca3a7917cc25102440e3 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Tue, 9 Apr 2024 11:17:46 +0800 Subject: [PATCH 09/45] Fix building OpenFst on Windows. (#744) --- cmake/openfst.cmake | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cmake/openfst.cmake b/cmake/openfst.cmake index b6633437f..cb58a54e9 100644 --- a/cmake/openfst.cmake +++ b/cmake/openfst.cmake @@ -3,18 +3,18 @@ function(download_openfst) include(FetchContent) - set(openfst_URL "https://github.com/csukuangfj/openfst/archive/792965fda2a3bc29f282321f527af0d6ba26fd22.zip") - set(openfst_URL2 "https://hub.nuaa.cf/csukuangfj/openfst/archive/792965fda2a3bc29f282321f527af0d6ba26fd22.zip") - set(openfst_HASH "SHA256=815d8acf555e4aaece294d6280ec209d0e9d91e0120e8406b24ff7124ecdbb26") + set(openfst_URL "https://github.com/csukuangfj/openfst/archive/refs/tags/sherpa-onnx-2024-04-09.tar.gz") + set(openfst_URL2 "https://hub.nuaa.cf/csukuangfj/openfst/archive/refs/tags/sherpa-onnx-2024-04-09.tar.gz") + set(openfst_HASH "SHA256=d6bdb1700fa38938807184c69a5abe133e730af80822bb85c8f228768a969b92") # If you don't have access to the Internet, # please pre-download it set(possible_file_locations - $ENV{HOME}/Downloads/openfst-792965fda2a3bc29f282321f527af0d6ba26fd22.zip - ${CMAKE_SOURCE_DIR}/openfst-792965fda2a3bc29f282321f527af0d6ba26fd22.zip - ${CMAKE_BINARY_DIR}/openfst-792965fda2a3bc29f282321f527af0d6ba26fd22.zip - /tmp/openfst-792965fda2a3bc29f282321f527af0d6ba26fd22.zip - /star-fj/fangjun/download/github/openfst-792965fda2a3bc29f282321f527af0d6ba26fd22.zip + $ENV{HOME}/Downloads/openfst-sherpa-onnx-2024-04-09.tar.gz + ${CMAKE_SOURCE_DIR}/openfst-sherpa-onnx-2024-04-09.tar.gz + ${CMAKE_BINARY_DIR}/openfst-sherpa-onnx-2024-04-09.tar.gz + /tmp/openfst-sherpa-onnx-2024-04-09.tar.gz + /star-fj/fangjun/download/github/openfst-sherpa-onnx-2024-04-09.tar.gz ) foreach(f IN LISTS possible_file_locations) From c9ae7595d51b7d2963ecf32ec1a33882de9d86d4 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Wed, 10 Apr 2024 09:56:35 +0800 Subject: [PATCH 10/45] Fix go API examples with portaudio on Windows. (#746) --- .../real-time-speech-recognition-from-microphone/README.md | 2 +- .../real-time-speech-recognition-from-microphone/main.go | 2 +- go-api-examples/vad-asr-paraformer/main.go | 2 +- go-api-examples/vad-asr-whisper/main.go | 2 +- go-api-examples/vad-speaker-identification/main.go | 2 +- go-api-examples/vad-spoken-language-identification/main.go | 2 +- go-api-examples/vad/main.go | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go-api-examples/real-time-speech-recognition-from-microphone/README.md b/go-api-examples/real-time-speech-recognition-from-microphone/README.md index c87c185a8..17d805278 100644 --- a/go-api-examples/real-time-speech-recognition-from-microphone/README.md +++ b/go-api-examples/real-time-speech-recognition-from-microphone/README.md @@ -3,7 +3,7 @@ This examples shows how to use the golang package of [sherpa-onnx][sherpa-onnx] for real-time speech recognition from microphone. -It uses +It uses to read the microphone and you have to install `portaudio` first. On macOS, you can use diff --git a/go-api-examples/real-time-speech-recognition-from-microphone/main.go b/go-api-examples/real-time-speech-recognition-from-microphone/main.go index 094132835..5cbd919f8 100644 --- a/go-api-examples/real-time-speech-recognition-from-microphone/main.go +++ b/go-api-examples/real-time-speech-recognition-from-microphone/main.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/gordonklaus/portaudio" + portaudio "github.com/csukuangfj/portaudio-go" sherpa "github.com/k2-fsa/sherpa-onnx-go/sherpa_onnx" flag "github.com/spf13/pflag" "log" diff --git a/go-api-examples/vad-asr-paraformer/main.go b/go-api-examples/vad-asr-paraformer/main.go index 54e1ed1c8..5311cfb44 100644 --- a/go-api-examples/vad-asr-paraformer/main.go +++ b/go-api-examples/vad-asr-paraformer/main.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/gordonklaus/portaudio" + portaudio "github.com/csukuangfj/portaudio-go" sherpa "github.com/k2-fsa/sherpa-onnx-go/sherpa_onnx" "log" "strings" diff --git a/go-api-examples/vad-asr-whisper/main.go b/go-api-examples/vad-asr-whisper/main.go index 85c675efb..d657aaa8c 100644 --- a/go-api-examples/vad-asr-whisper/main.go +++ b/go-api-examples/vad-asr-whisper/main.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/gordonklaus/portaudio" + portaudio "github.com/csukuangfj/portaudio-go" sherpa "github.com/k2-fsa/sherpa-onnx-go/sherpa_onnx" "log" "strings" diff --git a/go-api-examples/vad-speaker-identification/main.go b/go-api-examples/vad-speaker-identification/main.go index f2b69d092..317f3755b 100644 --- a/go-api-examples/vad-speaker-identification/main.go +++ b/go-api-examples/vad-speaker-identification/main.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/gordonklaus/portaudio" + portaudio "github.com/csukuangfj/portaudio-go" sherpa "github.com/k2-fsa/sherpa-onnx-go/sherpa_onnx" "log" ) diff --git a/go-api-examples/vad-spoken-language-identification/main.go b/go-api-examples/vad-spoken-language-identification/main.go index 71c375328..5661897fe 100644 --- a/go-api-examples/vad-spoken-language-identification/main.go +++ b/go-api-examples/vad-spoken-language-identification/main.go @@ -3,7 +3,7 @@ package main import ( "fmt" iso639 "github.com/barbashov/iso639-3" - "github.com/gordonklaus/portaudio" + portaudio "github.com/csukuangfj/portaudio-go" sherpa "github.com/k2-fsa/sherpa-onnx-go/sherpa_onnx" "log" ) diff --git a/go-api-examples/vad/main.go b/go-api-examples/vad/main.go index c9ab4b491..5a96ef3b4 100644 --- a/go-api-examples/vad/main.go +++ b/go-api-examples/vad/main.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/gordonklaus/portaudio" + portaudio "github.com/csukuangfj/portaudio-go" sherpa "github.com/k2-fsa/sherpa-onnx-go/sherpa_onnx" "log" ) From f20291cadcbe08a2772f51ad2df76d021a2f2541 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Wed, 10 Apr 2024 14:47:06 +0800 Subject: [PATCH 11/45] Support audio tagging using zipformer (#747) --- .github/scripts/test-audio-tagging.sh | 32 +++++ .github/workflows/linux.yaml | 10 ++ .github/workflows/macos.yaml | 10 ++ .github/workflows/windows-x64.yaml | 10 ++ .github/workflows/windows-x86.yaml | 9 ++ cmake/cmake_extension.py | 1 + go-api-examples/vad-asr-paraformer/.gitignore | 2 + nodejs-examples/test-offline-tts-zh.js | 2 +- sherpa-onnx/csrc/CMakeLists.txt | 12 ++ sherpa-onnx/csrc/audio-tagging-impl.cc | 23 ++++ sherpa-onnx/csrc/audio-tagging-impl.h | 29 +++++ sherpa-onnx/csrc/audio-tagging-label-file.cc | 70 +++++++++++ sherpa-onnx/csrc/audio-tagging-label-file.h | 31 +++++ .../csrc/audio-tagging-model-config.cc | 42 +++++++ sherpa-onnx/csrc/audio-tagging-model-config.h | 39 ++++++ .../csrc/audio-tagging-zipformer-impl.h | 95 ++++++++++++++ sherpa-onnx/csrc/audio-tagging.cc | 75 +++++++++++ sherpa-onnx/csrc/audio-tagging.h | 65 ++++++++++ sherpa-onnx/csrc/math.h | 7 +- sherpa-onnx/csrc/offline-stream.cc | 2 +- sherpa-onnx/csrc/offline-stream.h | 5 +- ...ne-zipformer-audio-tagging-model-config.cc | 40 ++++++ ...ine-zipformer-audio-tagging-model-config.h | 29 +++++ .../offline-zipformer-audio-tagging-model.cc | 118 ++++++++++++++++++ .../offline-zipformer-audio-tagging-model.h | 64 ++++++++++ .../csrc/offline-zipformer-ctc-model.cc | 2 + .../csrc/offline-zipformer-ctc-model.h | 1 - sherpa-onnx/csrc/session.cc | 6 + sherpa-onnx/csrc/session.h | 10 +- .../csrc/sherpa-onnx-offline-audio-tagging.cc | 97 ++++++++++++++ 30 files changed, 927 insertions(+), 11 deletions(-) create mode 100755 .github/scripts/test-audio-tagging.sh create mode 100644 go-api-examples/vad-asr-paraformer/.gitignore create mode 100644 sherpa-onnx/csrc/audio-tagging-impl.cc create mode 100644 sherpa-onnx/csrc/audio-tagging-impl.h create mode 100644 sherpa-onnx/csrc/audio-tagging-label-file.cc create mode 100644 sherpa-onnx/csrc/audio-tagging-label-file.h create mode 100644 sherpa-onnx/csrc/audio-tagging-model-config.cc create mode 100644 sherpa-onnx/csrc/audio-tagging-model-config.h create mode 100644 sherpa-onnx/csrc/audio-tagging-zipformer-impl.h create mode 100644 sherpa-onnx/csrc/audio-tagging.cc create mode 100644 sherpa-onnx/csrc/audio-tagging.h create mode 100644 sherpa-onnx/csrc/offline-zipformer-audio-tagging-model-config.cc create mode 100644 sherpa-onnx/csrc/offline-zipformer-audio-tagging-model-config.h create mode 100644 sherpa-onnx/csrc/offline-zipformer-audio-tagging-model.cc create mode 100644 sherpa-onnx/csrc/offline-zipformer-audio-tagging-model.h create mode 100644 sherpa-onnx/csrc/sherpa-onnx-offline-audio-tagging.cc diff --git a/.github/scripts/test-audio-tagging.sh b/.github/scripts/test-audio-tagging.sh new file mode 100755 index 000000000..57e6663fe --- /dev/null +++ b/.github/scripts/test-audio-tagging.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -ex + +log() { + # This function is from espnet + local fname=${BASH_SOURCE[1]##*/} + echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" +} + +echo "EXE is $EXE" +echo "PATH: $PATH" + +which $EXE + +log "------------------------------------------------------------" +log "Run zipformer for audio tagging " +log "------------------------------------------------------------" + +curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/audio-tagging-models/sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 +tar xvf sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 +rm sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 +repo=sherpa-onnx-zipformer-audio-tagging-2024-04-09 +ls -lh $repo + +for w in 1.wav 2.wav 3.wav 4.wav; do + $EXE \ + --zipformer-model=$repo/model.onnx \ + --labels=$repo/class_labels_indices.csv \ + $repo/test_wavs/$w +done +rm -rf $repo diff --git a/.github/workflows/linux.yaml b/.github/workflows/linux.yaml index b32362a3d..ae0aec470 100644 --- a/.github/workflows/linux.yaml +++ b/.github/workflows/linux.yaml @@ -15,6 +15,7 @@ on: - '.github/scripts/test-offline-ctc.sh' - '.github/scripts/test-online-ctc.sh' - '.github/scripts/test-offline-tts.sh' + - '.github/scripts/test-audio-tagging.sh' - 'CMakeLists.txt' - 'cmake/**' - 'sherpa-onnx/csrc/*' @@ -32,6 +33,7 @@ on: - '.github/scripts/test-offline-ctc.sh' - '.github/scripts/test-online-ctc.sh' - '.github/scripts/test-offline-tts.sh' + - '.github/scripts/test-audio-tagging.sh' - 'CMakeLists.txt' - 'cmake/**' - 'sherpa-onnx/csrc/*' @@ -124,6 +126,14 @@ jobs: name: release-${{ matrix.build_type }}-with-shared-lib-${{ matrix.shared_lib }}-with-tts-${{ matrix.with_tts }} path: build/bin/* + - name: Test Audio tagging + shell: bash + run: | + export PATH=$PWD/build/bin:$PATH + export EXE=sherpa-onnx-offline-audio-tagging + + .github/scripts/test-audio-tagging.sh + - name: Test online CTC shell: bash run: | diff --git a/.github/workflows/macos.yaml b/.github/workflows/macos.yaml index 0d0980619..9dfcb7c9d 100644 --- a/.github/workflows/macos.yaml +++ b/.github/workflows/macos.yaml @@ -15,6 +15,7 @@ on: - '.github/scripts/test-offline-ctc.sh' - '.github/scripts/test-offline-tts.sh' - '.github/scripts/test-online-ctc.sh' + - '.github/scripts/test-audio-tagging.sh' - 'CMakeLists.txt' - 'cmake/**' - 'sherpa-onnx/csrc/*' @@ -31,6 +32,7 @@ on: - '.github/scripts/test-offline-ctc.sh' - '.github/scripts/test-offline-tts.sh' - '.github/scripts/test-online-ctc.sh' + - '.github/scripts/test-audio-tagging.sh' - 'CMakeLists.txt' - 'cmake/**' - 'sherpa-onnx/csrc/*' @@ -103,6 +105,14 @@ jobs: otool -L build/bin/sherpa-onnx otool -l build/bin/sherpa-onnx + - name: Test Audio tagging + shell: bash + run: | + export PATH=$PWD/build/bin:$PATH + export EXE=sherpa-onnx-offline-audio-tagging + + .github/scripts/test-audio-tagging.sh + - name: Test C API shell: bash run: | diff --git a/.github/workflows/windows-x64.yaml b/.github/workflows/windows-x64.yaml index ea7cf7458..8f1715591 100644 --- a/.github/workflows/windows-x64.yaml +++ b/.github/workflows/windows-x64.yaml @@ -14,6 +14,7 @@ on: - '.github/scripts/test-offline-ctc.sh' - '.github/scripts/test-online-ctc.sh' - '.github/scripts/test-offline-tts.sh' + - '.github/scripts/test-audio-tagging.sh' - 'CMakeLists.txt' - 'cmake/**' - 'sherpa-onnx/csrc/*' @@ -28,6 +29,7 @@ on: - '.github/scripts/test-offline-ctc.sh' - '.github/scripts/test-online-ctc.sh' - '.github/scripts/test-offline-tts.sh' + - '.github/scripts/test-audio-tagging.sh' - 'CMakeLists.txt' - 'cmake/**' - 'sherpa-onnx/csrc/*' @@ -70,6 +72,14 @@ jobs: ls -lh ./bin/Release/sherpa-onnx.exe + - name: Test Audio tagging + shell: bash + run: | + export PATH=$PWD/build/bin/Release:$PATH + export EXE=sherpa-onnx-offline-audio-tagging.exe + + .github/scripts/test-audio-tagging.sh + - name: Test C API shell: bash run: | diff --git a/.github/workflows/windows-x86.yaml b/.github/workflows/windows-x86.yaml index 69ad7cd97..65d1bea62 100644 --- a/.github/workflows/windows-x86.yaml +++ b/.github/workflows/windows-x86.yaml @@ -14,6 +14,7 @@ on: - '.github/scripts/test-offline-ctc.sh' - '.github/scripts/test-offline-tts.sh' - '.github/scripts/test-online-ctc.sh' + - '.github/scripts/test-audio-tagging.sh' - 'CMakeLists.txt' - 'cmake/**' - 'sherpa-onnx/csrc/*' @@ -28,6 +29,7 @@ on: - '.github/scripts/test-offline-ctc.sh' - '.github/scripts/test-offline-tts.sh' - '.github/scripts/test-online-ctc.sh' + - '.github/scripts/test-audio-tagging.sh' - 'CMakeLists.txt' - 'cmake/**' - 'sherpa-onnx/csrc/*' @@ -85,6 +87,13 @@ jobs: # export EXE=sherpa-onnx-offline-language-identification.exe # # .github/scripts/test-spoken-language-identification.sh + - name: Test Audio tagging + shell: bash + run: | + export PATH=$PWD/build/bin/Release:$PATH + export EXE=sherpa-onnx-offline-audio-tagging.exe + + .github/scripts/test-audio-tagging.sh - name: Test online CTC shell: bash diff --git a/cmake/cmake_extension.py b/cmake/cmake_extension.py index 75b09a5c5..b78129b21 100644 --- a/cmake/cmake_extension.py +++ b/cmake/cmake_extension.py @@ -46,6 +46,7 @@ def enable_alsa(): def get_binaries(): binaries = [ "sherpa-onnx", + "sherpa-onnx-offline-audio-tagging", "sherpa-onnx-keyword-spotter", "sherpa-onnx-microphone", "sherpa-onnx-microphone-offline", diff --git a/go-api-examples/vad-asr-paraformer/.gitignore b/go-api-examples/vad-asr-paraformer/.gitignore new file mode 100644 index 000000000..66786c69b --- /dev/null +++ b/go-api-examples/vad-asr-paraformer/.gitignore @@ -0,0 +1,2 @@ +go.sum +vad-asr-paraformer diff --git a/nodejs-examples/test-offline-tts-zh.js b/nodejs-examples/test-offline-tts-zh.js index a53748c77..d777d490e 100644 --- a/nodejs-examples/test-offline-tts-zh.js +++ b/nodejs-examples/test-offline-tts-zh.js @@ -4,7 +4,7 @@ const sherpa_onnx = require('sherpa-onnx'); function createOfflineTts() { let offlineTtsVitsModelConfig = { - model: './vits-icefall-zh-aishell3/vits-aishell3.onnx', + model: './vits-icefall-zh-aishell3/model.onnx', lexicon: './vits-icefall-zh-aishell3/lexicon.txt', tokens: './vits-icefall-zh-aishell3/tokens.txt', dataDir: '', diff --git a/sherpa-onnx/csrc/CMakeLists.txt b/sherpa-onnx/csrc/CMakeLists.txt index bedd1ed2a..5b2e5941c 100644 --- a/sherpa-onnx/csrc/CMakeLists.txt +++ b/sherpa-onnx/csrc/CMakeLists.txt @@ -111,6 +111,16 @@ list(APPEND sources speaker-embedding-manager.cc ) +# audio tagging +list(APPEND sources + audio-tagging-impl.cc + audio-tagging-label-file.cc + audio-tagging-model-config.cc + audio-tagging.cc + offline-zipformer-audio-tagging-model-config.cc + offline-zipformer-audio-tagging-model.cc +) + if(SHERPA_ONNX_ENABLE_TTS) list(APPEND sources lexicon.cc @@ -193,6 +203,7 @@ if(SHERPA_ONNX_ENABLE_BINARY) add_executable(sherpa-onnx-offline sherpa-onnx-offline.cc) add_executable(sherpa-onnx-offline-parallel sherpa-onnx-offline-parallel.cc) add_executable(sherpa-onnx-offline-language-identification sherpa-onnx-offline-language-identification.cc) + add_executable(sherpa-onnx-offline-audio-tagging sherpa-onnx-offline-audio-tagging.cc) if(SHERPA_ONNX_ENABLE_TTS) add_executable(sherpa-onnx-offline-tts sherpa-onnx-offline-tts.cc) @@ -204,6 +215,7 @@ if(SHERPA_ONNX_ENABLE_BINARY) sherpa-onnx-offline sherpa-onnx-offline-parallel sherpa-onnx-offline-language-identification + sherpa-onnx-offline-audio-tagging ) if(SHERPA_ONNX_ENABLE_TTS) list(APPEND main_exes diff --git a/sherpa-onnx/csrc/audio-tagging-impl.cc b/sherpa-onnx/csrc/audio-tagging-impl.cc new file mode 100644 index 000000000..33e8dbb78 --- /dev/null +++ b/sherpa-onnx/csrc/audio-tagging-impl.cc @@ -0,0 +1,23 @@ +// sherpa-onnx/csrc/audio-tagging-impl.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/csrc/audio-tagging-impl.h" + +#include "sherpa-onnx/csrc/audio-tagging-zipformer-impl.h" +#include "sherpa-onnx/csrc/macros.h" + +namespace sherpa_onnx { + +std::unique_ptr AudioTaggingImpl::Create( + const AudioTaggingConfig &config) { + if (!config.model.zipformer.model.empty()) { + return std::make_unique(config); + } + + SHERPA_ONNX_LOG( + "Please specify an audio tagging model! Return a null pointer"); + return nullptr; +} + +} // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/audio-tagging-impl.h b/sherpa-onnx/csrc/audio-tagging-impl.h new file mode 100644 index 000000000..e5e192457 --- /dev/null +++ b/sherpa-onnx/csrc/audio-tagging-impl.h @@ -0,0 +1,29 @@ +// sherpa-onnx/csrc/audio-tagging-impl.h +// +// Copyright (c) 2024 Xiaomi Corporation +#ifndef SHERPA_ONNX_CSRC_AUDIO_TAGGING_IMPL_H_ +#define SHERPA_ONNX_CSRC_AUDIO_TAGGING_IMPL_H_ + +#include +#include + +#include "sherpa-onnx/csrc/audio-tagging.h" + +namespace sherpa_onnx { + +class AudioTaggingImpl { + public: + virtual ~AudioTaggingImpl() = default; + + static std::unique_ptr Create( + const AudioTaggingConfig &config); + + virtual std::unique_ptr CreateStream() const = 0; + + virtual std::vector Compute(OfflineStream *s, + int32_t top_k = -1) const = 0; +}; + +} // namespace sherpa_onnx + +#endif // SHERPA_ONNX_CSRC_AUDIO_TAGGING_IMPL_H_ diff --git a/sherpa-onnx/csrc/audio-tagging-label-file.cc b/sherpa-onnx/csrc/audio-tagging-label-file.cc new file mode 100644 index 000000000..24846a174 --- /dev/null +++ b/sherpa-onnx/csrc/audio-tagging-label-file.cc @@ -0,0 +1,70 @@ +// sherpa-onnx/csrc/audio-tagging-label-file.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/csrc/audio-tagging-label-file.h" + +#include +#include +#include + +#include "sherpa-onnx/csrc/macros.h" +#include "sherpa-onnx/csrc/text-utils.h" + +namespace sherpa_onnx { + +AudioTaggingLabels::AudioTaggingLabels(const std::string &filename) { + std::ifstream is(filename); + Init(is); +} + +// Format of a label file +/* +index,mid,display_name +0,/m/09x0r,"Speech" +1,/m/05zppz,"Male speech, man speaking" +*/ +void AudioTaggingLabels::Init(std::istream &is) { + std::string line; + std::getline(is, line); // skip the header + + std::string index; + std::string tmp; + std::string name; + + while (std::getline(is, line)) { + index.clear(); + name.clear(); + std::istringstream input2(line); + + std::getline(input2, index, ','); + std::getline(input2, tmp, ','); + std::getline(input2, name); + + std::size_t pos{}; + int32_t i = std::stoi(index, &pos); + if (index.size() == 0 || pos != index.size()) { + SHERPA_ONNX_LOGE("Invalid line: %s", line.c_str()); + exit(-1); + } + + if (i != names_.size()) { + SHERPA_ONNX_LOGE( + "Index should be sorted and contiguous. Expected index: %d, given: " + "%d.", + static_cast(names_.size()), i); + } + if (name.empty() || name.front() != '"' || name.back() != '"') { + SHERPA_ONNX_LOGE("Invalid line: %s", line.c_str()); + exit(-1); + } + + names_.emplace_back(name.begin() + 1, name.end() - 1); + } +} + +const std::string &AudioTaggingLabels::GetEventName(int32_t index) const { + return names_.at(index); +} + +} // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/audio-tagging-label-file.h b/sherpa-onnx/csrc/audio-tagging-label-file.h new file mode 100644 index 000000000..9e71557f5 --- /dev/null +++ b/sherpa-onnx/csrc/audio-tagging-label-file.h @@ -0,0 +1,31 @@ +// sherpa-onnx/csrc/audio-tagging-label-file.h +// +// Copyright (c) 2024 Xiaomi Corporation +#ifndef SHERPA_ONNX_CSRC_AUDIO_TAGGING_LABEL_FILE_H_ +#define SHERPA_ONNX_CSRC_AUDIO_TAGGING_LABEL_FILE_H_ + +#include +#include +#include + +namespace sherpa_onnx { + +class AudioTaggingLabels { + public: + explicit AudioTaggingLabels(const std::string &filename); + + // Return the event name for the given index. + // The returned reference is valid as long as this object is alive + const std::string &GetEventName(int32_t index) const; + int32_t NumEventClasses() const { return names_.size(); } + + private: + void Init(std::istream &is); + + private: + std::vector names_; +}; + +} // namespace sherpa_onnx + +#endif // SHERPA_ONNX_CSRC_AUDIO_TAGGING_LABEL_FILE_H_ diff --git a/sherpa-onnx/csrc/audio-tagging-model-config.cc b/sherpa-onnx/csrc/audio-tagging-model-config.cc new file mode 100644 index 000000000..f1f526f80 --- /dev/null +++ b/sherpa-onnx/csrc/audio-tagging-model-config.cc @@ -0,0 +1,42 @@ +// sherpa-onnx/csrc/audio-tagging-model-config.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/csrc/audio-tagging-model-config.h" + +namespace sherpa_onnx { + +void AudioTaggingModelConfig::Register(ParseOptions *po) { + zipformer.Register(po); + + po->Register("num-threads", &num_threads, + "Number of threads to run the neural network"); + + po->Register("debug", &debug, + "true to print model information while loading it."); + + po->Register("provider", &provider, + "Specify a provider to use: cpu, cuda, coreml"); +} + +bool AudioTaggingModelConfig::Validate() const { + if (!zipformer.model.empty() && !zipformer.Validate()) { + return false; + } + + return true; +} + +std::string AudioTaggingModelConfig::ToString() const { + std::ostringstream os; + + os << "AudioTaggingModelConfig("; + os << "zipformer=" << zipformer.ToString() << ", "; + os << "num_threads=" << num_threads << ", "; + os << "debug=" << (debug ? "True" : "False") << ", "; + os << "provider=\"" << provider << "\")"; + + return os.str(); +} + +} // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/audio-tagging-model-config.h b/sherpa-onnx/csrc/audio-tagging-model-config.h new file mode 100644 index 000000000..862e9bf9e --- /dev/null +++ b/sherpa-onnx/csrc/audio-tagging-model-config.h @@ -0,0 +1,39 @@ +// sherpa-onnx/csrc/audio-tagging-model-config.h +// +// Copyright (c) 2024 Xiaomi Corporation +#ifndef SHERPA_ONNX_CSRC_AUDIO_TAGGING_MODEL_CONFIG_H_ +#define SHERPA_ONNX_CSRC_AUDIO_TAGGING_MODEL_CONFIG_H_ + +#include + +#include "sherpa-onnx/csrc/offline-zipformer-audio-tagging-model-config.h" +#include "sherpa-onnx/csrc/parse-options.h" + +namespace sherpa_onnx { + +struct AudioTaggingModelConfig { + struct OfflineZipformerAudioTaggingModelConfig zipformer; + + int32_t num_threads = 1; + bool debug = false; + std::string provider = "cpu"; + + AudioTaggingModelConfig() = default; + + AudioTaggingModelConfig( + const OfflineZipformerAudioTaggingModelConfig &zipformer, + int32_t num_threads, bool debug, const std::string &provider) + : zipformer(zipformer), + num_threads(num_threads), + debug(debug), + provider(provider) {} + + void Register(ParseOptions *po); + bool Validate() const; + + std::string ToString() const; +}; + +} // namespace sherpa_onnx + +#endif // SHERPA_ONNX_CSRC_AUDIO_TAGGING_MODEL_CONFIG_H_ diff --git a/sherpa-onnx/csrc/audio-tagging-zipformer-impl.h b/sherpa-onnx/csrc/audio-tagging-zipformer-impl.h new file mode 100644 index 000000000..639f644c8 --- /dev/null +++ b/sherpa-onnx/csrc/audio-tagging-zipformer-impl.h @@ -0,0 +1,95 @@ +// sherpa-onnx/csrc/audio-tagging-zipformer-impl.h +// +// Copyright (c) 2024 Xiaomi Corporation +#ifndef SHERPA_ONNX_CSRC_AUDIO_TAGGING_ZIPFORMER_IMPL_H_ +#define SHERPA_ONNX_CSRC_AUDIO_TAGGING_ZIPFORMER_IMPL_H_ + +#include +#include +#include + +#include "sherpa-onnx/csrc/audio-tagging-impl.h" +#include "sherpa-onnx/csrc/audio-tagging-label-file.h" +#include "sherpa-onnx/csrc/audio-tagging.h" +#include "sherpa-onnx/csrc/macros.h" +#include "sherpa-onnx/csrc/math.h" +#include "sherpa-onnx/csrc/offline-zipformer-audio-tagging-model.h" + +namespace sherpa_onnx { + +class AudioTaggingZipformerImpl : public AudioTaggingImpl { + public: + explicit AudioTaggingZipformerImpl(const AudioTaggingConfig &config) + : config_(config), model_(config.model), labels_(config.labels) { + if (model_.NumEventClasses() != labels_.NumEventClasses()) { + SHERPA_ONNX_LOGE("number of classes: %d (model) != %d (label file)", + model_.NumEventClasses(), labels_.NumEventClasses()); + exit(-1); + } + } + + std::unique_ptr CreateStream() const override { + return std::make_unique(); + } + + std::vector Compute(OfflineStream *s, + int32_t top_k = -1) const override { + if (top_k < 0) { + top_k = config_.top_k; + } + + int32_t num_event_classes = model_.NumEventClasses(); + + if (top_k > num_event_classes) { + top_k = num_event_classes; + } + + auto memory_info = + Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeDefault); + + // WARNING(fangjun): It is fixed to 80 for all models from icefall + int32_t feat_dim = 80; + std::vector f = s->GetFrames(); + + int32_t num_frames = f.size() / feat_dim; + + std::array shape = {1, num_frames, feat_dim}; + + Ort::Value x = Ort::Value::CreateTensor(memory_info, f.data(), f.size(), + shape.data(), shape.size()); + + int64_t x_length_scalar = num_frames; + std::array x_length_shape = {1}; + Ort::Value x_length = + Ort::Value::CreateTensor(memory_info, &x_length_scalar, 1, + x_length_shape.data(), x_length_shape.size()); + + Ort::Value probs = model_.Forward(std::move(x), std::move(x_length)); + + const float *p = probs.GetTensorData(); + + std::vector top_k_indexes = TopkIndex(p, num_event_classes, top_k); + + std::vector ans(top_k); + + int32_t i = 0; + + for (int32_t index : top_k_indexes) { + ans[i].name = labels_.GetEventName(index); + ans[i].index = index; + ans[i].prob = p[index]; + i += 1; + } + + return ans; + } + + private: + AudioTaggingConfig config_; + OfflineZipformerAudioTaggingModel model_; + AudioTaggingLabels labels_; +}; + +} // namespace sherpa_onnx + +#endif // SHERPA_ONNX_CSRC_AUDIO_TAGGING_ZIPFORMER_IMPL_H_ diff --git a/sherpa-onnx/csrc/audio-tagging.cc b/sherpa-onnx/csrc/audio-tagging.cc new file mode 100644 index 000000000..34d558dd9 --- /dev/null +++ b/sherpa-onnx/csrc/audio-tagging.cc @@ -0,0 +1,75 @@ +// sherpa-onnx/csrc/audio-tagging.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/csrc/audio-tagging.h" + +#include "sherpa-onnx/csrc/audio-tagging-impl.h" +#include "sherpa-onnx/csrc/file-utils.h" +#include "sherpa-onnx/csrc/macros.h" + +namespace sherpa_onnx { + +std::string AudioEvent::ToString() const { + std::ostringstream os; + os << "AudioEvent("; + os << "name=\"" << name << "\", "; + os << "index=" << index << ", "; + os << "prob=" << prob << ")"; + return os.str(); +} + +void AudioTaggingConfig::Register(ParseOptions *po) { + model.Register(po); + po->Register("labels", &labels, "Event label file"); + po->Register("top-k", &top_k, "Top k events to return in the result"); +} + +bool AudioTaggingConfig::Validate() const { + if (!model.Validate()) { + return false; + } + + if (top_k < 1) { + SHERPA_ONNX_LOGE("--top-k should be >= 1. Given: %d", top_k); + return false; + } + + if (labels.empty()) { + SHERPA_ONNX_LOGE("Please provide --labels"); + return false; + } + + if (!FileExists(labels)) { + SHERPA_ONNX_LOGE("--labels %s does not exist", labels.c_str()); + return false; + } + + return true; +} +std::string AudioTaggingConfig::ToString() const { + std::ostringstream os; + + os << "AudioTaggingConfig("; + os << "model=" << model.ToString() << ", "; + os << "labels=\"" << labels << "\", "; + os << "top_k=" << top_k << ")"; + + return os.str(); +} + +AudioTagging::AudioTagging(const AudioTaggingConfig &config) + : impl_(AudioTaggingImpl::Create(config)) {} + +AudioTagging::~AudioTagging() = default; + +std::unique_ptr AudioTagging::CreateStream() const { + return impl_->CreateStream(); +} + +std::vector AudioTagging::Compute(OfflineStream *s, + int32_t top_k /*= -1*/) const { + return impl_->Compute(s, top_k); +} + +} // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/audio-tagging.h b/sherpa-onnx/csrc/audio-tagging.h new file mode 100644 index 000000000..50cfea02c --- /dev/null +++ b/sherpa-onnx/csrc/audio-tagging.h @@ -0,0 +1,65 @@ +// sherpa-onnx/csrc/audio-tagging.h +// +// Copyright (c) 2024 Xiaomi Corporation +#ifndef SHERPA_ONNX_CSRC_AUDIO_TAGGING_H_ +#define SHERPA_ONNX_CSRC_AUDIO_TAGGING_H_ + +#include +#include +#include + +#include "sherpa-onnx/csrc/audio-tagging-model-config.h" +#include "sherpa-onnx/csrc/offline-stream.h" +#include "sherpa-onnx/csrc/parse-options.h" + +namespace sherpa_onnx { + +struct AudioTaggingConfig { + AudioTaggingModelConfig model; + std::string labels; + + int32_t top_k = 5; + + AudioTaggingConfig() = default; + + AudioTaggingConfig(const AudioTaggingModelConfig &model, + const std::string &labels, int32_t top_k) + : model(model), labels(labels), top_k(top_k) {} + + void Register(ParseOptions *po); + bool Validate() const; + + std::string ToString() const; +}; + +struct AudioEvent { + std::string name; // name of the event + int32_t index; // index of the event in the label file + float prob; // probability of the event + + std::string ToString() const; +}; + +class AudioTaggingImpl; + +class AudioTagging { + public: + explicit AudioTagging(const AudioTaggingConfig &config); + + ~AudioTagging(); + + std::unique_ptr CreateStream() const; + + // If top_k is -1, then config.top_k is used. + // Otherwise, config.top_k is ignored + // + // Return top_k AudioEvent. ans[0].prob is the largest of all returned events. + std::vector Compute(OfflineStream *s, int32_t top_k = -1) const; + + private: + std::unique_ptr impl_; +}; + +} // namespace sherpa_onnx + +#endif // SHERPA_ONNX_CSRC_AUDIO_TAGGING_H_ diff --git a/sherpa-onnx/csrc/math.h b/sherpa-onnx/csrc/math.h index ba01835fe..121a05aeb 100644 --- a/sherpa-onnx/csrc/math.h +++ b/sherpa-onnx/csrc/math.h @@ -97,8 +97,8 @@ void LogSoftmax(T *in, int32_t w, int32_t h) { } template -void SubtractBlank(T *in, int32_t w, int32_t h, - int32_t blank_idx, float blank_penalty) { +void SubtractBlank(T *in, int32_t w, int32_t h, int32_t blank_idx, + float blank_penalty) { for (int32_t i = 0; i != h; ++i) { in[blank_idx] -= blank_penalty; in += w; @@ -116,8 +116,7 @@ std::vector TopkIndex(const T *vec, int32_t size, int32_t topk) { }); int32_t k_num = std::min(size, topk); - std::vector index(vec_index.begin(), vec_index.begin() + k_num); - return index; + return {vec_index.begin(), vec_index.begin() + k_num}; } } // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/offline-stream.cc b/sherpa-onnx/csrc/offline-stream.cc index 08e601363..0eea103c9 100644 --- a/sherpa-onnx/csrc/offline-stream.cc +++ b/sherpa-onnx/csrc/offline-stream.cc @@ -234,7 +234,7 @@ OfflineStream::OfflineStream( : impl_(std::make_unique(config, context_graph)) {} OfflineStream::OfflineStream(WhisperTag tag, - ContextGraphPtr context_graph /*= nullptr*/) + ContextGraphPtr context_graph /*= {}*/) : impl_(std::make_unique(tag, context_graph)) {} OfflineStream::~OfflineStream() = default; diff --git a/sherpa-onnx/csrc/offline-stream.h b/sherpa-onnx/csrc/offline-stream.h index 26b890b60..08ddbd316 100644 --- a/sherpa-onnx/csrc/offline-stream.h +++ b/sherpa-onnx/csrc/offline-stream.h @@ -71,10 +71,9 @@ struct WhisperTag {}; class OfflineStream { public: explicit OfflineStream(const OfflineFeatureExtractorConfig &config = {}, - ContextGraphPtr context_graph = nullptr); + ContextGraphPtr context_graph = {}); - explicit OfflineStream(WhisperTag tag, - ContextGraphPtr context_graph = nullptr); + explicit OfflineStream(WhisperTag tag, ContextGraphPtr context_graph = {}); ~OfflineStream(); /** diff --git a/sherpa-onnx/csrc/offline-zipformer-audio-tagging-model-config.cc b/sherpa-onnx/csrc/offline-zipformer-audio-tagging-model-config.cc new file mode 100644 index 000000000..3034ff77f --- /dev/null +++ b/sherpa-onnx/csrc/offline-zipformer-audio-tagging-model-config.cc @@ -0,0 +1,40 @@ +// sherpa-onnx/csrc/offline-zipformer-audio-tagging-model-config.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/csrc/offline-zipformer-audio-tagging-model-config.h" + +#include "sherpa-onnx/csrc/file-utils.h" +#include "sherpa-onnx/csrc/macros.h" + +namespace sherpa_onnx { + +void OfflineZipformerAudioTaggingModelConfig::Register(ParseOptions *po) { + po->Register("zipformer-model", &model, + "Path to zipformer model for audio tagging"); +} + +bool OfflineZipformerAudioTaggingModelConfig::Validate() const { + if (model.empty()) { + SHERPA_ONNX_LOGE("Please provide --zipformer-model"); + return false; + } + + if (!FileExists(model)) { + SHERPA_ONNX_LOGE("--zipformer-model: %s does not exist", model.c_str()); + return false; + } + + return true; +} + +std::string OfflineZipformerAudioTaggingModelConfig::ToString() const { + std::ostringstream os; + + os << "OfflineZipformerAudioTaggingModelConfig("; + os << "model=\"" << model << "\")"; + + return os.str(); +} + +} // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/offline-zipformer-audio-tagging-model-config.h b/sherpa-onnx/csrc/offline-zipformer-audio-tagging-model-config.h new file mode 100644 index 000000000..4f60e832e --- /dev/null +++ b/sherpa-onnx/csrc/offline-zipformer-audio-tagging-model-config.h @@ -0,0 +1,29 @@ +// sherpa-onnx/csrc/offline-zipformer-audio-tagging-model-config.h +// +// Copyright (c) 2024 Xiaomi Corporation +#ifndef SHERPA_ONNX_CSRC_OFFLINE_ZIPFORMER_AUDIO_TAGGING_MODEL_CONFIG_H_ +#define SHERPA_ONNX_CSRC_OFFLINE_ZIPFORMER_AUDIO_TAGGING_MODEL_CONFIG_H_ + +#include + +#include "sherpa-onnx/csrc/parse-options.h" + +namespace sherpa_onnx { + +struct OfflineZipformerAudioTaggingModelConfig { + std::string model; + + OfflineZipformerAudioTaggingModelConfig() = default; + + explicit OfflineZipformerAudioTaggingModelConfig(const std::string &model) + : model(model) {} + + void Register(ParseOptions *po); + bool Validate() const; + + std::string ToString() const; +}; + +} // namespace sherpa_onnx + +#endif // SHERPA_ONNX_CSRC_OFFLINE_ZIPFORMER_AUDIO_TAGGING_MODEL_CONFIG_H_ diff --git a/sherpa-onnx/csrc/offline-zipformer-audio-tagging-model.cc b/sherpa-onnx/csrc/offline-zipformer-audio-tagging-model.cc new file mode 100644 index 000000000..8a2e80dc2 --- /dev/null +++ b/sherpa-onnx/csrc/offline-zipformer-audio-tagging-model.cc @@ -0,0 +1,118 @@ +// sherpa-onnx/csrc/offline-zipformer-audio-tagging-model.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/csrc/offline-zipformer-audio-tagging-model.h" + +#include +#include + +#include "sherpa-onnx/csrc/onnx-utils.h" +#include "sherpa-onnx/csrc/session.h" +#include "sherpa-onnx/csrc/text-utils.h" + +namespace sherpa_onnx { + +class OfflineZipformerAudioTaggingModel::Impl { + public: + explicit Impl(const AudioTaggingModelConfig &config) + : config_(config), + env_(ORT_LOGGING_LEVEL_ERROR), + sess_opts_(GetSessionOptions(config)), + allocator_{} { + auto buf = ReadFile(config_.zipformer.model); + Init(buf.data(), buf.size()); + } + +#if __ANDROID_API__ >= 9 + Impl(AAssetManager *mgr, const AudioTaggingModelConfig &config) + : config_(config), + env_(ORT_LOGGING_LEVEL_ERROR), + sess_opts_(GetSessionOptions(config)), + allocator_{} { + auto buf = ReadFile(mgr, config_.zipformer.model); + Init(buf.data(), buf.size()); + } +#endif + + Ort::Value Forward(Ort::Value features, Ort::Value features_length) { + std::array inputs = {std::move(features), + std::move(features_length)}; + + auto ans = + sess_->Run({}, input_names_ptr_.data(), inputs.data(), inputs.size(), + output_names_ptr_.data(), output_names_ptr_.size()); + return std::move(ans[0]); + } + + int32_t NumEventClasses() const { return num_event_classes_; } + + OrtAllocator *Allocator() const { return allocator_; } + + private: + void Init(void *model_data, size_t model_data_length) { + sess_ = std::make_unique(env_, model_data, model_data_length, + sess_opts_); + + GetInputNames(sess_.get(), &input_names_, &input_names_ptr_); + + GetOutputNames(sess_.get(), &output_names_, &output_names_ptr_); + + // get meta data + Ort::ModelMetadata meta_data = sess_->GetModelMetadata(); + if (config_.debug) { + std::ostringstream os; + PrintModelMetadata(os, meta_data); + SHERPA_ONNX_LOGE("%s\n", os.str().c_str()); + } + + // get num_event_classes from the output[0].shape, + // which is (N, num_event_classes) + num_event_classes_ = + sess_->GetOutputTypeInfo(0).GetTensorTypeAndShapeInfo().GetShape()[1]; + } + + private: + AudioTaggingModelConfig config_; + Ort::Env env_; + Ort::SessionOptions sess_opts_; + Ort::AllocatorWithDefaultOptions allocator_; + + std::unique_ptr sess_; + + std::vector input_names_; + std::vector input_names_ptr_; + + std::vector output_names_; + std::vector output_names_ptr_; + + int32_t num_event_classes_ = 0; +}; + +OfflineZipformerAudioTaggingModel::OfflineZipformerAudioTaggingModel( + const AudioTaggingModelConfig &config) + : impl_(std::make_unique(config)) {} + +#if __ANDROID_API__ >= 9 +OfflineZipformerAudioTaggingModel::OfflineZipformerAudioTaggingModel( + AAssetManager *mgr, const AudioTaggingModelConfig &config) + : impl_(std::make_unique(mgr, config)) {} +#endif + +OfflineZipformerAudioTaggingModel::~OfflineZipformerAudioTaggingModel() = + default; + +Ort::Value OfflineZipformerAudioTaggingModel::Forward( + Ort::Value features, Ort::Value features_length) const { + return impl_->Forward(std::move(features), std::move(features_length)); +} + +int32_t OfflineZipformerAudioTaggingModel::NumEventClasses() const { + return impl_->NumEventClasses(); +} + +OrtAllocator *OfflineZipformerAudioTaggingModel::Allocator() const { + return impl_->Allocator(); +} + +} // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/offline-zipformer-audio-tagging-model.h b/sherpa-onnx/csrc/offline-zipformer-audio-tagging-model.h new file mode 100644 index 000000000..282823499 --- /dev/null +++ b/sherpa-onnx/csrc/offline-zipformer-audio-tagging-model.h @@ -0,0 +1,64 @@ +// sherpa-onnx/csrc/offline-zipformer-audio-tagging-model.h +// +// Copyright (c) 2024 Xiaomi Corporation +#ifndef SHERPA_ONNX_CSRC_OFFLINE_ZIPFORMER_AUDIO_TAGGING_MODEL_H_ +#define SHERPA_ONNX_CSRC_OFFLINE_ZIPFORMER_AUDIO_TAGGING_MODEL_H_ +#include +#include + +#if __ANDROID_API__ >= 9 +#include "android/asset_manager.h" +#include "android/asset_manager_jni.h" +#endif + +#include "onnxruntime_cxx_api.h" // NOLINT +#include "sherpa-onnx/csrc/audio-tagging-model-config.h" + +namespace sherpa_onnx { + +/** This class implements the zipformer CTC model of the librispeech recipe + * from icefall. + * + * See + * https://github.com/k2-fsa/icefall/blob/master/egs/audioset/AT/zipformer/export-onnx.py + */ +class OfflineZipformerAudioTaggingModel { + public: + explicit OfflineZipformerAudioTaggingModel( + const AudioTaggingModelConfig &config); + +#if __ANDROID_API__ >= 9 + OfflineZipformerAudioTaggingModel(AAssetManager *mgr, + const AudioTaggingModelConfig &config); +#endif + + ~OfflineZipformerAudioTaggingModel(); + + /** Run the forward method of the model. + * + * @param features A tensor of shape (N, T, C). + * @param features_length A 1-D tensor of shape (N,) containing number of + * valid frames in `features` before padding. + * Its dtype is int64_t. + * + * @return Return a tensor + * - probs: A 2-D tensor of shape (N, num_event_classes). + */ + Ort::Value Forward(Ort::Value features, Ort::Value features_length) const; + + /** Return the number of event classes of the model + */ + int32_t NumEventClasses() const; + + /** Return an allocator for allocating memory + */ + OrtAllocator *Allocator() const; + + private: + class Impl; + std::unique_ptr impl_; +}; + +} // namespace sherpa_onnx + +#endif // SHERPA_ONNX_CSRC_OFFLINE_ZIPFORMER_AUDIO_TAGGING_MODEL_H_ diff --git a/sherpa-onnx/csrc/offline-zipformer-ctc-model.cc b/sherpa-onnx/csrc/offline-zipformer-ctc-model.cc index a82ef6255..8db9439e4 100644 --- a/sherpa-onnx/csrc/offline-zipformer-ctc-model.cc +++ b/sherpa-onnx/csrc/offline-zipformer-ctc-model.cc @@ -4,6 +4,8 @@ #include "sherpa-onnx/csrc/offline-zipformer-ctc-model.h" +#include + #include "sherpa-onnx/csrc/macros.h" #include "sherpa-onnx/csrc/onnx-utils.h" #include "sherpa-onnx/csrc/session.h" diff --git a/sherpa-onnx/csrc/offline-zipformer-ctc-model.h b/sherpa-onnx/csrc/offline-zipformer-ctc-model.h index e3b9a05ce..c4e835636 100644 --- a/sherpa-onnx/csrc/offline-zipformer-ctc-model.h +++ b/sherpa-onnx/csrc/offline-zipformer-ctc-model.h @@ -4,7 +4,6 @@ #ifndef SHERPA_ONNX_CSRC_OFFLINE_ZIPFORMER_CTC_MODEL_H_ #define SHERPA_ONNX_CSRC_OFFLINE_ZIPFORMER_CTC_MODEL_H_ #include -#include #include #include diff --git a/sherpa-onnx/csrc/session.cc b/sherpa-onnx/csrc/session.cc index aacd1e158..d555ed7a7 100644 --- a/sherpa-onnx/csrc/session.cc +++ b/sherpa-onnx/csrc/session.cc @@ -140,9 +140,11 @@ Ort::SessionOptions GetSessionOptions(const VadModelConfig &config) { return GetSessionOptionsImpl(config.num_threads, config.provider); } +#if SHERPA_ONNX_ENABLE_TTS Ort::SessionOptions GetSessionOptions(const OfflineTtsModelConfig &config) { return GetSessionOptionsImpl(config.num_threads, config.provider); } +#endif Ort::SessionOptions GetSessionOptions( const SpeakerEmbeddingExtractorConfig &config) { @@ -154,4 +156,8 @@ Ort::SessionOptions GetSessionOptions( return GetSessionOptionsImpl(config.num_threads, config.provider); } +Ort::SessionOptions GetSessionOptions(const AudioTaggingModelConfig &config) { + return GetSessionOptionsImpl(config.num_threads, config.provider); +} + } // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/session.h b/sherpa-onnx/csrc/session.h index 9bb3e4371..94f263fd9 100644 --- a/sherpa-onnx/csrc/session.h +++ b/sherpa-onnx/csrc/session.h @@ -6,15 +6,19 @@ #define SHERPA_ONNX_CSRC_SESSION_H_ #include "onnxruntime_cxx_api.h" // NOLINT +#include "sherpa-onnx/csrc/audio-tagging-model-config.h" #include "sherpa-onnx/csrc/offline-lm-config.h" #include "sherpa-onnx/csrc/offline-model-config.h" -#include "sherpa-onnx/csrc/offline-tts-model-config.h" #include "sherpa-onnx/csrc/online-lm-config.h" #include "sherpa-onnx/csrc/online-model-config.h" #include "sherpa-onnx/csrc/speaker-embedding-extractor.h" #include "sherpa-onnx/csrc/spoken-language-identification.h" #include "sherpa-onnx/csrc/vad-model-config.h" +#if SHERPA_ONNX_ENABLE_TTS +#include "sherpa-onnx/csrc/offline-tts-model-config.h" +#endif + namespace sherpa_onnx { Ort::SessionOptions GetSessionOptions(const OnlineModelConfig &config); @@ -27,7 +31,9 @@ Ort::SessionOptions GetSessionOptions(const OnlineLMConfig &config); Ort::SessionOptions GetSessionOptions(const VadModelConfig &config); +#if SHERPA_ONNX_ENABLE_TTS Ort::SessionOptions GetSessionOptions(const OfflineTtsModelConfig &config); +#endif Ort::SessionOptions GetSessionOptions( const SpeakerEmbeddingExtractorConfig &config); @@ -35,6 +41,8 @@ Ort::SessionOptions GetSessionOptions( Ort::SessionOptions GetSessionOptions( const SpokenLanguageIdentificationConfig &config); +Ort::SessionOptions GetSessionOptions(const AudioTaggingModelConfig &config); + } // namespace sherpa_onnx #endif // SHERPA_ONNX_CSRC_SESSION_H_ diff --git a/sherpa-onnx/csrc/sherpa-onnx-offline-audio-tagging.cc b/sherpa-onnx/csrc/sherpa-onnx-offline-audio-tagging.cc new file mode 100644 index 000000000..862818f5c --- /dev/null +++ b/sherpa-onnx/csrc/sherpa-onnx-offline-audio-tagging.cc @@ -0,0 +1,97 @@ +// sherpa-onnx/csrc/sherpa-onnx-offline-audio-tagging.cc +// +// Copyright (c) 2024 Xiaomi Corporation +#include + +#include "sherpa-onnx/csrc/audio-tagging.h" +#include "sherpa-onnx/csrc/parse-options.h" +#include "sherpa-onnx/csrc/wave-reader.h" + +int32_t main(int32_t argc, char *argv[]) { + const char *kUsageMessage = R"usage( +Audio tagging from a file. + +Usage: + +wget https://github.com/k2-fsa/sherpa-onnx/releases/download/audio-tagging-models/sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 +tar xvf sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 +rm sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 + +./bin/sherpa-onnx-offline-audio-tagging \ + --zipformer-model=./sherpa-onnx-zipformer-audio-tagging-2024-04-09/model.onnx \ + --labels=./sherpa-onnx-zipformer-audio-tagging-2024-04-09/class_labels_indices.csv \ + sherpa-onnx-zipformer-audio-tagging-2024-04-09/test_wavs/0.wav + +Input wave files should be of single channel, 16-bit PCM encoded wave file; its +sampling rate can be arbitrary and does not need to be 16kHz. + +Please see +https://github.com/k2-fsa/sherpa-onnx/releases/tag/audio-tagging-models +for more models. +)usage"; + + sherpa_onnx::ParseOptions po(kUsageMessage); + sherpa_onnx::AudioTaggingConfig config; + config.Register(&po); + po.Read(argc, argv); + + if (po.NumArgs() != 1) { + fprintf(stderr, "\nError: Please provide 1 wave file\n\n"); + po.PrintUsage(); + exit(EXIT_FAILURE); + } + + fprintf(stderr, "%s\n", config.ToString().c_str()); + + if (!config.Validate()) { + fprintf(stderr, "Errors in config!\n"); + return -1; + } + + sherpa_onnx::AudioTagging tagger(config); + std::string wav_filename = po.GetArg(1); + + int32_t sampling_rate = -1; + + bool is_ok = false; + const std::vector samples = + sherpa_onnx::ReadWave(wav_filename, &sampling_rate, &is_ok); + + if (!is_ok) { + fprintf(stderr, "Failed to read %s\n", wav_filename.c_str()); + return -1; + } + + const float duration = samples.size() / static_cast(sampling_rate); + + fprintf(stderr, "Start to compute\n"); + const auto begin = std::chrono::steady_clock::now(); + + auto stream = tagger.CreateStream(); + + stream->AcceptWaveform(sampling_rate, samples.data(), samples.size()); + + auto results = tagger.Compute(stream.get()); + const auto end = std::chrono::steady_clock::now(); + fprintf(stderr, "Done\n"); + + int32_t i = 0; + + for (const auto &event : results) { + fprintf(stderr, "%d: %s\n", i, event.ToString().c_str()); + i += 1; + } + + float elapsed_seconds = + std::chrono::duration_cast(end - begin) + .count() / + 1000.; + float rtf = elapsed_seconds / duration; + fprintf(stderr, "Num threads: %d\n", config.model.num_threads); + fprintf(stderr, "Wave duration: %.3f\n", duration); + fprintf(stderr, "Elapsed seconds: %.3f s\n", elapsed_seconds); + fprintf(stderr, "Real time factor (RTF): %.3f / %.3f = %.3f\n", + elapsed_seconds, duration, rtf); + + return 0; +} From 042976ea6e2df3cb3d705a3d6474fc948485f294 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Wed, 10 Apr 2024 21:00:35 +0800 Subject: [PATCH 12/45] Add C++ microphone examples for audio tagging (#749) --- .github/workflows/test-build-wheel.yaml | 2 +- .github/workflows/test-pip-install.yaml | 2 +- README.md | 33 ++- android/README.md | 16 +- .../asr-microphone-example/c-api-alsa.cc | 2 +- cmake/cmake_extension.py | 2 + ...microphone-with-endpoint-detection-alsa.py | 2 +- python-api-examples/vad-alsa.py | 2 +- .../vad-remove-non-speech-segments-alsa.py | 2 +- sherpa-onnx/csrc/CMakeLists.txt | 8 + sherpa-onnx/csrc/alsa.cc | 2 +- .../sherpa-onnx-alsa-offline-audio-tagging.cc | 190 ++++++++++++++ ...nnx-alsa-offline-speaker-identification.cc | 4 +- sherpa-onnx/csrc/sherpa-onnx-alsa-offline.cc | 6 +- sherpa-onnx/csrc/sherpa-onnx-alsa.cc | 2 +- .../csrc/sherpa-onnx-keyword-spotter-alsa.cc | 4 +- .../sherpa-onnx-keyword-spotter-microphone.cc | 40 ++- ...a-onnx-microphone-offline-audio-tagging.cc | 238 ++++++++++++++++++ ...crophone-offline-speaker-identification.cc | 37 ++- .../csrc/sherpa-onnx-microphone-offline.cc | 35 ++- sherpa-onnx/csrc/sherpa-onnx-microphone.cc | 34 ++- sherpa-onnx/csrc/sherpa-onnx-vad-alsa.cc | 2 +- .../sherpa-onnx-vad-microphone-offline-asr.cc | 48 +++- .../csrc/sherpa-onnx-vad-microphone.cc | 53 +++- 24 files changed, 706 insertions(+), 60 deletions(-) create mode 100644 sherpa-onnx/csrc/sherpa-onnx-alsa-offline-audio-tagging.cc create mode 100644 sherpa-onnx/csrc/sherpa-onnx-microphone-offline-audio-tagging.cc diff --git a/.github/workflows/test-build-wheel.yaml b/.github/workflows/test-build-wheel.yaml index 54c265bbd..c7c36f871 100644 --- a/.github/workflows/test-build-wheel.yaml +++ b/.github/workflows/test-build-wheel.yaml @@ -89,7 +89,7 @@ jobs: export PATH=/c/hostedtoolcache/windows/Python/3.8.10/x64/bin:$PATH export PATH=/c/hostedtoolcache/windows/Python/3.9.13/x64/bin:$PATH export PATH=/c/hostedtoolcache/windows/Python/3.10.11/x64/bin:$PATH - export PATH=/c/hostedtoolcache/windows/Python/3.11.8/x64/bin:$PATH + export PATH=/c/hostedtoolcache/windows/Python/3.11.9/x64/bin:$PATH export PATH=/c/hostedtoolcache/windows/Python/3.12.2/x64/bin:$PATH which sherpa-onnx diff --git a/.github/workflows/test-pip-install.yaml b/.github/workflows/test-pip-install.yaml index c79f3a8b3..381df814f 100644 --- a/.github/workflows/test-pip-install.yaml +++ b/.github/workflows/test-pip-install.yaml @@ -67,7 +67,7 @@ jobs: export PATH=/c/hostedtoolcache/windows/Python/3.8.10/x64/bin:$PATH export PATH=/c/hostedtoolcache/windows/Python/3.9.13/x64/bin:$PATH export PATH=/c/hostedtoolcache/windows/Python/3.10.11/x64/bin:$PATH - export PATH=/c/hostedtoolcache/windows/Python/3.11.8/x64/bin:$PATH + export PATH=/c/hostedtoolcache/windows/Python/3.11.9/x64/bin:$PATH export PATH=/c/hostedtoolcache/windows/Python/3.12.2/x64/bin:$PATH sherpa-onnx --help diff --git a/README.md b/README.md index 7ea1b638e..4f4246ebb 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,48 @@ This repository supports running the following functions **locally** - - Speech-to-text (i.e., ASR) + - Speech-to-text (i.e., ASR); both streaming and non-streaming are supported - Text-to-speech (i.e., TTS) - Speaker identification + - Speaker verification + - Spoken language identification + - Audio tagging + - VAD (e.g., [silero-vad](https://github.com/snakers4/silero-vad)) on the following platforms and operating systems: - - Linux, macOS, Windows - - Android + - x86, ``x86_64``, 32-bit ARM, 64-bit ARM (arm64, aarch64), RISC-V (riscv64) + - Linux, macOS, Windows, openKylin + - Android, WearOS - iOS - - Raspberry Pi + - NodeJS + - WebAssembly + - [Raspberry Pi](https://www.raspberrypi.com/) + - [RV1126](https://www.rock-chips.com/uploads/pdf/2022.8.26/191/RV1126%20Brief%20Datasheet.pdf) + - [LicheePi4A](https://sipeed.com/licheepi4a) + - [VisionFive 2](https://www.starfivetech.com/en/site/boards) + - [旭日X3派](https://developer.horizon.ai/api/v1/fileData/documents_pi/index.html) - etc +with the following APIs + + - C++ + - C + - Python + - Go + - ``C#`` + - Javascript + - Java + - Kotlin + - Swift + # Useful links - Documentation: https://k2-fsa.github.io/sherpa/onnx/ - APK for the text-to-speech engine: https://k2-fsa.github.io/sherpa/onnx/tts/apk-engine.html - APK for speaker identification: https://k2-fsa.github.io/sherpa/onnx/speaker-identification/apk.html +- APK for speech recognition: https://github.com/k2-fsa/sherpa-onnx/releases/ +- Bilibili 演示视频: https://search.bilibili.com/all?keyword=%E6%96%B0%E4%B8%80%E4%BB%A3Kaldi # How to reach us diff --git a/android/README.md b/android/README.md index 053ad66e0..705049f8f 100644 --- a/android/README.md +++ b/android/README.md @@ -7,14 +7,22 @@ for usage. - [SherpaOnnx](./SherpaOnnx) It uses a streaming ASR model. - [SherpaOnnx2Pass](./SherpaOnnx2Pass) It uses a streaming ASR model - for the first pass and use a non-streaming ASR model for the second pass. + for the first pass and use a non-streaming ASR model for the second pass -- [SherpaOnnxVad](./SherpaOnnxVad) It demonstrates how to use a VAD +- [SherpaOnnxKws](./SherpaOnnxKws) It demonstrates how to use keyword spotting -- [SherpaOnnxVadAsr](./SherpaOnnxVadAsr) It uses a VAD with a non-streaming - ASR model. +- [SherpaOnnxSpeakerIdentification](./SherpaOnnxSpeakerIdentification) It demonstrates + how to use speaker identification - [SherpaOnnxTts](./SherpaOnnxTts) It is for standalone text-to-speech. - [SherpaOnnxTtsEngine](./SherpaOnnxTtsEngine) It is for text-to-speech engine; you can use it to replace the system TTS engine. + +- [SherpaOnnxVad](./SherpaOnnxVad) It demonstrates how to use a VAD + +- [SherpaOnnxVadAsr](./SherpaOnnxVadAsr) It uses a VAD with a non-streaming + ASR model. + +- [SherpaOnnxWebSocket](./SherpaOnnxWebSocket) It shows how to write a websocket + client for the Python streaming websocket server. diff --git a/c-api-examples/asr-microphone-example/c-api-alsa.cc b/c-api-examples/asr-microphone-example/c-api-alsa.cc index caa5d8c6b..a1df63ad4 100644 --- a/c-api-examples/asr-microphone-example/c-api-alsa.cc +++ b/c-api-examples/asr-microphone-example/c-api-alsa.cc @@ -99,7 +99,7 @@ card 3: UACDemoV10 [UACDemoV1.0], device 0: USB Audio [USB Audio] Subdevices: 1/1 Subdevice #0: subdevice #0 -and if you want to select card 3 and the device 0 on that card, please use: +and if you want to select card 3 and device 0 on that card, please use: plughw:3,0 diff --git a/cmake/cmake_extension.py b/cmake/cmake_extension.py index b78129b21..ca57ff303 100644 --- a/cmake/cmake_extension.py +++ b/cmake/cmake_extension.py @@ -50,6 +50,7 @@ def get_binaries(): "sherpa-onnx-keyword-spotter", "sherpa-onnx-microphone", "sherpa-onnx-microphone-offline", + "sherpa-onnx-microphone-offline-audio-tagging", "sherpa-onnx-microphone-offline-speaker-identification", "sherpa-onnx-offline", "sherpa-onnx-offline-language-identification", @@ -69,6 +70,7 @@ def get_binaries(): "sherpa-onnx-alsa-offline-speaker-identification", "sherpa-onnx-offline-tts-play-alsa", "sherpa-onnx-vad-alsa", + "sherpa-onnx-alsa-offline-audio-tagging", ] if is_windows(): diff --git a/python-api-examples/speech-recognition-from-microphone-with-endpoint-detection-alsa.py b/python-api-examples/speech-recognition-from-microphone-with-endpoint-detection-alsa.py index 45962755f..81d5ae9b5 100755 --- a/python-api-examples/speech-recognition-from-microphone-with-endpoint-detection-alsa.py +++ b/python-api-examples/speech-recognition-from-microphone-with-endpoint-detection-alsa.py @@ -123,7 +123,7 @@ def get_args(): Subdevices: 1/1 Subdevice #0: subdevice #0 -and if you want to select card 3 and the device 0 on that card, please use: +and if you want to select card 3 and device 0 on that card, please use: plughw:3,0 diff --git a/python-api-examples/vad-alsa.py b/python-api-examples/vad-alsa.py index 8f23d477e..259869c01 100755 --- a/python-api-examples/vad-alsa.py +++ b/python-api-examples/vad-alsa.py @@ -39,7 +39,7 @@ def get_args(): Subdevices: 1/1 Subdevice #0: subdevice #0 -and if you want to select card 3 and the device 0 on that card, please use: +and if you want to select card 3 and device 0 on that card, please use: plughw:3,0 diff --git a/python-api-examples/vad-remove-non-speech-segments-alsa.py b/python-api-examples/vad-remove-non-speech-segments-alsa.py index 34f88e40f..6d93bb1e9 100755 --- a/python-api-examples/vad-remove-non-speech-segments-alsa.py +++ b/python-api-examples/vad-remove-non-speech-segments-alsa.py @@ -68,7 +68,7 @@ def get_args(): Subdevices: 1/1 Subdevice #0: subdevice #0 -and if you want to select card 3 and the device 0 on that card, please use: +and if you want to select card 3 and device 0 on that card, please use: plughw:3,0 diff --git a/sherpa-onnx/csrc/CMakeLists.txt b/sherpa-onnx/csrc/CMakeLists.txt index 5b2e5941c..fe2a1a939 100644 --- a/sherpa-onnx/csrc/CMakeLists.txt +++ b/sherpa-onnx/csrc/CMakeLists.txt @@ -264,6 +264,7 @@ if(SHERPA_ONNX_HAS_ALSA AND SHERPA_ONNX_ENABLE_BINARY) add_executable(sherpa-onnx-alsa-offline sherpa-onnx-alsa-offline.cc alsa.cc) add_executable(sherpa-onnx-alsa-offline-speaker-identification sherpa-onnx-alsa-offline-speaker-identification.cc alsa.cc) add_executable(sherpa-onnx-vad-alsa sherpa-onnx-vad-alsa.cc alsa.cc) + add_executable(sherpa-onnx-alsa-offline-audio-tagging sherpa-onnx-alsa-offline-audio-tagging.cc alsa.cc) if(SHERPA_ONNX_ENABLE_TTS) @@ -276,6 +277,7 @@ if(SHERPA_ONNX_HAS_ALSA AND SHERPA_ONNX_ENABLE_BINARY) sherpa-onnx-alsa-offline-speaker-identification sherpa-onnx-keyword-spotter-alsa sherpa-onnx-vad-alsa + sherpa-onnx-alsa-offline-audio-tagging ) if(SHERPA_ONNX_ENABLE_TTS) @@ -354,6 +356,11 @@ if(SHERPA_ONNX_ENABLE_PORTAUDIO AND SHERPA_ONNX_ENABLE_BINARY) microphone.cc ) + add_executable(sherpa-onnx-microphone-offline-audio-tagging + sherpa-onnx-microphone-offline-audio-tagging.cc + microphone.cc + ) + if(BUILD_SHARED_LIBS) set(PA_LIB portaudio) else() @@ -365,6 +372,7 @@ if(SHERPA_ONNX_ENABLE_PORTAUDIO AND SHERPA_ONNX_ENABLE_BINARY) sherpa-onnx-keyword-spotter-microphone sherpa-onnx-microphone-offline sherpa-onnx-microphone-offline-speaker-identification + sherpa-onnx-microphone-offline-audio-tagging sherpa-onnx-vad-microphone sherpa-onnx-vad-microphone-offline-asr ) diff --git a/sherpa-onnx/csrc/alsa.cc b/sherpa-onnx/csrc/alsa.cc index 3c883331a..a65761099 100644 --- a/sherpa-onnx/csrc/alsa.cc +++ b/sherpa-onnx/csrc/alsa.cc @@ -35,7 +35,7 @@ card 3: UACDemoV10 [UACDemoV1.0], device 0: USB Audio [USB Audio] Subdevices: 1/1 Subdevice #0: subdevice #0 -and if you want to select card 3 and the device 0 on that card, please use: +and if you want to select card 3 and device 0 on that card, please use: plughw:3,0 diff --git a/sherpa-onnx/csrc/sherpa-onnx-alsa-offline-audio-tagging.cc b/sherpa-onnx/csrc/sherpa-onnx-alsa-offline-audio-tagging.cc new file mode 100644 index 000000000..6a5b701ea --- /dev/null +++ b/sherpa-onnx/csrc/sherpa-onnx-alsa-offline-audio-tagging.cc @@ -0,0 +1,190 @@ +// sherpa-onnx/csrc/sherpa-onnx-alsa-offline-audio-tagging.cc +// +// Copyright (c) 2022-2024 Xiaomi Corporation + +#include +#include +#include + +#include +#include // NOLINT +#include // NOLINT + +#include "sherpa-onnx/csrc/alsa.h" +#include "sherpa-onnx/csrc/audio-tagging.h" +#include "sherpa-onnx/csrc/macros.h" + +enum class State { + kIdle, + kRecording, + kDecoding, +}; + +State state = State::kIdle; + +// true to stop the program and exit +bool stop = false; + +std::vector samples; +std::mutex samples_mutex; + +static void DetectKeyPress() { + SHERPA_ONNX_LOGE("Press Enter to start"); + int32_t key; + while (!stop && (key = getchar())) { + if (key != 0x0a) { + continue; + } + + switch (state) { + case State::kIdle: + SHERPA_ONNX_LOGE("Start recording. Press Enter to stop recording"); + state = State::kRecording; + { + std::lock_guard lock(samples_mutex); + samples.clear(); + } + break; + case State::kRecording: + SHERPA_ONNX_LOGE("Stop recording. Decoding ..."); + state = State::kDecoding; + break; + case State::kDecoding: + break; + } + } +} + +static void Record(const char *device_name, int32_t expected_sample_rate) { + sherpa_onnx::Alsa alsa(device_name); + + if (alsa.GetExpectedSampleRate() != expected_sample_rate) { + fprintf(stderr, "sample rate: %d != %d\n", alsa.GetExpectedSampleRate(), + expected_sample_rate); + exit(-1); + } + + int32_t chunk = 0.1 * alsa.GetActualSampleRate(); + while (!stop) { + const std::vector &s = alsa.Read(chunk); + std::lock_guard lock(samples_mutex); + samples.insert(samples.end(), s.begin(), s.end()); + } +} + +static void Handler(int32_t sig) { + stop = true; + fprintf(stderr, "\nCaught Ctrl + C. Press Enter to exit\n"); +} + +int32_t main(int32_t argc, char *argv[]) { + signal(SIGINT, Handler); + + const char *kUsageMessage = R"usage( +Audio tagging from microphone (Linux only). +Usage: + +wget https://github.com/k2-fsa/sherpa-onnx/releases/download/audio-tagging-models/sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 +tar xvf sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 +rm sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 + +./bin/sherpa-onnx-alsa-offline-audio-tagging \ + --zipformer-model=./sherpa-onnx-zipformer-audio-tagging-2024-04-09/model.onnx \ + --labels=./sherpa-onnx-zipformer-audio-tagging-2024-04-09/class_labels_indices.csv \ + device_name + +Please refer to +https://github.com/k2-fsa/sherpa-onnx/releases/tag/audio-tagging-models +for a list of pre-trained models to download. + +The device name specifies which microphone to use in case there are several +on your system. You can use + + arecord -l + +to find all available microphones on your computer. For instance, if it outputs + +**** List of CAPTURE Hardware Devices **** +card 3: UACDemoV10 [UACDemoV1.0], device 0: USB Audio [USB Audio] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + +and if you want to select card 3 and device 0 on that card, please use: + + plughw:3,0 + +as the device_name. +)usage"; + + sherpa_onnx::ParseOptions po(kUsageMessage); + sherpa_onnx::AudioTaggingConfig config; + config.Register(&po); + + po.Read(argc, argv); + if (po.NumArgs() != 1) { + fprintf(stderr, "Please provide only 1 argument: the device name\n"); + po.PrintUsage(); + exit(EXIT_FAILURE); + } + + fprintf(stderr, "%s\n", config.ToString().c_str()); + + if (!config.Validate()) { + fprintf(stderr, "Errors in config!\n"); + return -1; + } + + SHERPA_ONNX_LOGE("Creating audio tagger ..."); + sherpa_onnx::AudioTagging tagger(config); + SHERPA_ONNX_LOGE("Audio tagger created created!"); + + std::string device_name = po.GetArg(1); + fprintf(stderr, "Use recording device: %s\n", device_name.c_str()); + + int32_t sample_rate = 16000; // fixed to 16000Hz for all models from icefall + + std::thread t2(Record, device_name.c_str(), sample_rate); + using namespace std::chrono_literals; // NOLINT + std::this_thread::sleep_for(100ms); // sleep for 100ms + std::thread t(DetectKeyPress); + + while (!stop) { + switch (state) { + case State::kIdle: + break; + case State::kRecording: + break; + case State::kDecoding: { + std::vector buf; + { + std::lock_guard lock(samples_mutex); + buf = std::move(samples); + } + SHERPA_ONNX_LOGE("Computing..."); + auto s = tagger.CreateStream(); + s->AcceptWaveform(sample_rate, buf.data(), buf.size()); + auto results = tagger.Compute(s.get()); + SHERPA_ONNX_LOGE("Result is:"); + + int32_t i = 0; + std::ostringstream os; + for (const auto &event : results) { + os << i << ": " << event.ToString() << "\n"; + i += 1; + } + + SHERPA_ONNX_LOGE("\n%s\n", os.str().c_str()); + + state = State::kIdle; + SHERPA_ONNX_LOGE("Press Enter to start"); + break; + } + } + + std::this_thread::sleep_for(20ms); // sleep for 20ms + } + t.join(); + t2.join(); + + return 0; +} diff --git a/sherpa-onnx/csrc/sherpa-onnx-alsa-offline-speaker-identification.cc b/sherpa-onnx/csrc/sherpa-onnx-alsa-offline-speaker-identification.cc index 76695d5cf..f4702a836 100644 --- a/sherpa-onnx/csrc/sherpa-onnx-alsa-offline-speaker-identification.cc +++ b/sherpa-onnx/csrc/sherpa-onnx-alsa-offline-speaker-identification.cc @@ -71,8 +71,8 @@ static void Record(const char *device_name, int32_t expected_sample_rate) { int32_t chunk = 0.1 * alsa.GetActualSampleRate(); while (!stop) { - std::lock_guard lock(samples_mutex); const std::vector &s = alsa.Read(chunk); + std::lock_guard lock(samples_mutex); samples.insert(samples.end(), s.begin(), s.end()); } } @@ -193,7 +193,7 @@ card 3: UACDemoV10 [UACDemoV1.0], device 0: USB Audio [USB Audio] Subdevices: 1/1 Subdevice #0: subdevice #0 -and if you want to select card 3 and the device 0 on that card, please use: +and if you want to select card 3 and device 0 on that card, please use: plughw:3,0 as the device_name. diff --git a/sherpa-onnx/csrc/sherpa-onnx-alsa-offline.cc b/sherpa-onnx/csrc/sherpa-onnx-alsa-offline.cc index 2f24a21a6..b69ec6cd1 100644 --- a/sherpa-onnx/csrc/sherpa-onnx-alsa-offline.cc +++ b/sherpa-onnx/csrc/sherpa-onnx-alsa-offline.cc @@ -68,8 +68,8 @@ static void Record(const char *device_name, int32_t expected_sample_rate) { int32_t chunk = 0.1 * alsa.GetActualSampleRate(); while (!stop) { - std::lock_guard lock(samples_mutex); const std::vector &s = alsa.Read(chunk); + std::lock_guard lock(samples_mutex); samples.insert(samples.end(), s.begin(), s.end()); } } @@ -119,7 +119,7 @@ Please refer to for a list of pre-trained models to download. The device name specifies which microphone to use in case there are several -on you system. You can use +on your system. You can use arecord -l @@ -130,7 +130,7 @@ card 3: UACDemoV10 [UACDemoV1.0], device 0: USB Audio [USB Audio] Subdevices: 1/1 Subdevice #0: subdevice #0 -and if you want to select card 3 and the device 0 on that card, please use: +and if you want to select card 3 and device 0 on that card, please use: plughw:3,0 diff --git a/sherpa-onnx/csrc/sherpa-onnx-alsa.cc b/sherpa-onnx/csrc/sherpa-onnx-alsa.cc index ccd909bb3..a0c4e3d64 100644 --- a/sherpa-onnx/csrc/sherpa-onnx-alsa.cc +++ b/sherpa-onnx/csrc/sherpa-onnx-alsa.cc @@ -52,7 +52,7 @@ card 3: UACDemoV10 [UACDemoV1.0], device 0: USB Audio [USB Audio] Subdevices: 1/1 Subdevice #0: subdevice #0 -and if you want to select card 3 and the device 0 on that card, please use: +and if you want to select card 3 and device 0 on that card, please use: plughw:3,0 diff --git a/sherpa-onnx/csrc/sherpa-onnx-keyword-spotter-alsa.cc b/sherpa-onnx/csrc/sherpa-onnx-keyword-spotter-alsa.cc index 2e784ebb8..a909ff250 100644 --- a/sherpa-onnx/csrc/sherpa-onnx-keyword-spotter-alsa.cc +++ b/sherpa-onnx/csrc/sherpa-onnx-keyword-spotter-alsa.cc @@ -40,7 +40,7 @@ Please refer to for a list of pre-trained models to download. The device name specifies which microphone to use in case there are several -on you system. You can use +on your system. You can use arecord -l @@ -51,7 +51,7 @@ card 3: UACDemoV10 [UACDemoV1.0], device 0: USB Audio [USB Audio] Subdevices: 1/1 Subdevice #0: subdevice #0 -and if you want to select card 3 and the device 0 on that card, please use: +and if you want to select card 3 and device 0 on that card, please use: plughw:3,0 diff --git a/sherpa-onnx/csrc/sherpa-onnx-keyword-spotter-microphone.cc b/sherpa-onnx/csrc/sherpa-onnx-keyword-spotter-microphone.cc index 1f42da40a..6100ba451 100644 --- a/sherpa-onnx/csrc/sherpa-onnx-keyword-spotter-microphone.cc +++ b/sherpa-onnx/csrc/sherpa-onnx-keyword-spotter-microphone.cc @@ -10,10 +10,11 @@ #include "portaudio.h" // NOLINT #include "sherpa-onnx/csrc/display.h" -#include "sherpa-onnx/csrc/microphone.h" #include "sherpa-onnx/csrc/keyword-spotter.h" +#include "sherpa-onnx/csrc/microphone.h" bool stop = false; +float mic_sample_rate = 16000; static int32_t RecordCallback(const void *input_buffer, void * /*output_buffer*/, @@ -23,7 +24,8 @@ static int32_t RecordCallback(const void *input_buffer, void *user_data) { auto stream = reinterpret_cast(user_data); - stream->AcceptWaveform(16000, reinterpret_cast(input_buffer), + stream->AcceptWaveform(mic_sample_rate, + reinterpret_cast(input_buffer), frames_per_buffer); return stop ? paComplete : paContinue; @@ -80,14 +82,31 @@ for a list of pre-trained models to download. PaDeviceIndex num_devices = Pa_GetDeviceCount(); fprintf(stderr, "Num devices: %d\n", num_devices); - PaStreamParameters param; + int32_t device_index = Pa_GetDefaultInputDevice(); - param.device = Pa_GetDefaultInputDevice(); - if (param.device == paNoDevice) { + if (device_index == paNoDevice) { fprintf(stderr, "No default input device found\n"); + fprintf(stderr, "If you are using Linux, please switch to \n"); + fprintf(stderr, " ./bin/sherpa-onnx-keyword-spotter-alsa \n"); exit(EXIT_FAILURE); } - fprintf(stderr, "Use default device: %d\n", param.device); + + const char *pDeviceIndex = std::getenv("SHERPA_ONNX_MIC_DEVICE"); + if (pDeviceIndex) { + fprintf(stderr, "Use specified device: %s\n", pDeviceIndex); + device_index = atoi(pDeviceIndex); + } + + for (int32_t i = 0; i != num_devices; ++i) { + const PaDeviceInfo *info = Pa_GetDeviceInfo(i); + fprintf(stderr, " %s %d %s\n", (i == device_index) ? "*" : " ", i, + info->name); + } + + PaStreamParameters param; + param.device = device_index; + + fprintf(stderr, "Use device: %d\n", param.device); const PaDeviceInfo *info = Pa_GetDeviceInfo(param.device); fprintf(stderr, " Name: %s\n", info->name); @@ -98,12 +117,19 @@ for a list of pre-trained models to download. param.suggestedLatency = info->defaultLowInputLatency; param.hostApiSpecificStreamInfo = nullptr; + + const char *pSampleRateStr = std::getenv("SHERPA_ONNX_MIC_SAMPLE_RATE"); + if (pSampleRateStr) { + fprintf(stderr, "Use sample rate %f for mic\n", mic_sample_rate); + mic_sample_rate = atof(pSampleRateStr); + } + float sample_rate = 16000; PaStream *stream; PaError err = Pa_OpenStream(&stream, ¶m, nullptr, /* &outputParameters, */ - sample_rate, + mic_sample_rate, 0, // frames per buffer paClipOff, // we won't output out of range samples // so don't bother clipping them diff --git a/sherpa-onnx/csrc/sherpa-onnx-microphone-offline-audio-tagging.cc b/sherpa-onnx/csrc/sherpa-onnx-microphone-offline-audio-tagging.cc new file mode 100644 index 000000000..169b995f8 --- /dev/null +++ b/sherpa-onnx/csrc/sherpa-onnx-microphone-offline-audio-tagging.cc @@ -0,0 +1,238 @@ +// sherpa-onnx/csrc/sherpa-onnx-microphone-offline-audio-tagging.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include +#include +#include + +#include +#include // std::tolower +#include // NOLINT +#include // NOLINT + +#include "portaudio.h" // NOLINT +#include "sherpa-onnx/csrc/audio-tagging.h" +#include "sherpa-onnx/csrc/macros.h" +#include "sherpa-onnx/csrc/microphone.h" + +enum class State { + kIdle, + kRecording, + kDecoding, +}; + +State state = State::kIdle; + +// true to stop the program and exit +bool stop = false; + +std::vector samples; +std::mutex samples_mutex; + +static void DetectKeyPress() { + SHERPA_ONNX_LOGE("Press Enter to start"); + int32_t key; + while (!stop && (key = getchar())) { + if (key != 0x0a) { + continue; + } + + switch (state) { + case State::kIdle: + SHERPA_ONNX_LOGE("Start recording. Press Enter to stop recording"); + state = State::kRecording; + { + std::lock_guard lock(samples_mutex); + samples.clear(); + } + break; + case State::kRecording: + SHERPA_ONNX_LOGE("Stop recording. Decoding ..."); + state = State::kDecoding; + break; + case State::kDecoding: + break; + } + } +} + +static int32_t RecordCallback(const void *input_buffer, + void * /*output_buffer*/, + unsigned long frames_per_buffer, // NOLINT + const PaStreamCallbackTimeInfo * /*time_info*/, + PaStreamCallbackFlags /*status_flags*/, + void *user_data) { + std::lock_guard lock(samples_mutex); + + auto p = reinterpret_cast(input_buffer); + samples.insert(samples.end(), p, p + frames_per_buffer); + + return stop ? paComplete : paContinue; +} + +static void Handler(int32_t sig) { + stop = true; + fprintf(stderr, "\nCaught Ctrl + C. Press Enter to exit\n"); +} + +int32_t main(int32_t argc, char *argv[]) { + signal(SIGINT, Handler); + + const char *kUsageMessage = R"usage( +Audio tagging from microphone. +Usage: + +wget https://github.com/k2-fsa/sherpa-onnx/releases/download/audio-tagging-models/sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 +tar xvf sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 +rm sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 + +./bin/sherpa-onnx-microphone-offline-audio-tagging \ + --zipformer-model=./sherpa-onnx-zipformer-audio-tagging-2024-04-09/model.onnx \ + --labels=./sherpa-onnx-zipformer-audio-tagging-2024-04-09/class_labels_indices.csv + +Please see +https://github.com/k2-fsa/sherpa-onnx/releases/tag/audio-tagging-models +for more models. +)usage"; + + sherpa_onnx::ParseOptions po(kUsageMessage); + sherpa_onnx::AudioTaggingConfig config; + config.Register(&po); + + po.Read(argc, argv); + if (po.NumArgs() != 0) { + fprintf(stderr, "\nThis program does not support positional arguments\n\n"); + po.PrintUsage(); + exit(EXIT_FAILURE); + } + + fprintf(stderr, "%s\n", config.ToString().c_str()); + + if (!config.Validate()) { + fprintf(stderr, "Errors in config!\n"); + return -1; + } + + SHERPA_ONNX_LOGE("Creating audio tagger ..."); + sherpa_onnx::AudioTagging tagger(config); + SHERPA_ONNX_LOGE("Audio tagger created created!"); + + sherpa_onnx::Microphone mic; + + PaDeviceIndex num_devices = Pa_GetDeviceCount(); + fprintf(stderr, "Num devices: %d\n", num_devices); + + int32_t device_index = Pa_GetDefaultInputDevice(); + + if (device_index == paNoDevice) { + fprintf(stderr, "No default input device found\n"); + fprintf(stderr, "If you are using Linux, please switch to \n"); + fprintf(stderr, " ./bin/sherpa-onnx-alsa-offline-audio-tagging \n"); + exit(EXIT_FAILURE); + } + + const char *pDeviceIndex = std::getenv("SHERPA_ONNX_MIC_DEVICE"); + if (pDeviceIndex) { + fprintf(stderr, "Use specified device: %s\n", pDeviceIndex); + device_index = atoi(pDeviceIndex); + } + + for (int32_t i = 0; i != num_devices; ++i) { + const PaDeviceInfo *info = Pa_GetDeviceInfo(i); + fprintf(stderr, " %s %d %s\n", (i == device_index) ? "*" : " ", i, + info->name); + } + + PaStreamParameters param; + param.device = device_index; + + fprintf(stderr, "Use device: %d\n", param.device); + + const PaDeviceInfo *info = Pa_GetDeviceInfo(param.device); + fprintf(stderr, " Name: %s\n", info->name); + fprintf(stderr, " Max input channels: %d\n", info->maxInputChannels); + + param.channelCount = 1; + param.sampleFormat = paFloat32; + + param.suggestedLatency = info->defaultLowInputLatency; + param.hostApiSpecificStreamInfo = nullptr; + float mic_sample_rate = 16000; + const char *pSampleRateStr = std::getenv("SHERPA_ONNX_MIC_SAMPLE_RATE"); + if (pSampleRateStr) { + fprintf(stderr, "Use sample rate %f for mic\n", mic_sample_rate); + mic_sample_rate = atof(pSampleRateStr); + } + + float sample_rate = 16000; + + PaStream *stream; + PaError err = + Pa_OpenStream(&stream, ¶m, nullptr, /* &outputParameters, */ + mic_sample_rate, + 0, // frames per buffer + paClipOff, // we won't output out of range samples + // so don't bother clipping them + RecordCallback, nullptr); + if (err != paNoError) { + fprintf(stderr, "portaudio error: %s\n", Pa_GetErrorText(err)); + exit(EXIT_FAILURE); + } + + err = Pa_StartStream(stream); + fprintf(stderr, "Started\n"); + + if (err != paNoError) { + fprintf(stderr, "portaudio error: %s\n", Pa_GetErrorText(err)); + exit(EXIT_FAILURE); + } + + std::thread t(DetectKeyPress); + while (!stop) { + switch (state) { + case State::kIdle: + break; + case State::kRecording: + break; + case State::kDecoding: { + std::vector buf; + { + std::lock_guard lock(samples_mutex); + buf = std::move(samples); + } + + SHERPA_ONNX_LOGE("Computing..."); + auto s = tagger.CreateStream(); + s->AcceptWaveform(mic_sample_rate, buf.data(), buf.size()); + auto results = tagger.Compute(s.get()); + + SHERPA_ONNX_LOGE("Result is:"); + + int32_t i = 0; + std::ostringstream os; + for (const auto &event : results) { + os << i << ": " << event.ToString() << "\n"; + i += 1; + } + + SHERPA_ONNX_LOGE("\n%s\n", os.str().c_str()); + + state = State::kIdle; + SHERPA_ONNX_LOGE("Press Enter to start"); + break; + } + } + + Pa_Sleep(20); // sleep for 20ms + } + t.join(); + + err = Pa_CloseStream(stream); + if (err != paNoError) { + fprintf(stderr, "portaudio error: %s\n", Pa_GetErrorText(err)); + exit(EXIT_FAILURE); + } + + return 0; +} diff --git a/sherpa-onnx/csrc/sherpa-onnx-microphone-offline-speaker-identification.cc b/sherpa-onnx/csrc/sherpa-onnx-microphone-offline-speaker-identification.cc index f525f5223..769e8b5ff 100644 --- a/sherpa-onnx/csrc/sherpa-onnx-microphone-offline-speaker-identification.cc +++ b/sherpa-onnx/csrc/sherpa-onnx-microphone-offline-speaker-identification.cc @@ -223,14 +223,31 @@ Note that `zh` means Chinese, while `en` means English. PaDeviceIndex num_devices = Pa_GetDeviceCount(); fprintf(stderr, "Num devices: %d\n", num_devices); - PaStreamParameters param; - - param.device = Pa_GetDefaultInputDevice(); - if (param.device == paNoDevice) { + int32_t device_index = Pa_GetDefaultInputDevice(); + if (device_index == paNoDevice) { fprintf(stderr, "No default input device found\n"); + fprintf(stderr, "If you are using Linux, please switch to \n"); + fprintf(stderr, + " ./bin/sherpa-onnx-alsa-offline-speaker-identification \n"); exit(EXIT_FAILURE); } - fprintf(stderr, "Use default device: %d\n", param.device); + + const char *pDeviceIndex = std::getenv("SHERPA_ONNX_MIC_DEVICE"); + if (pDeviceIndex) { + fprintf(stderr, "Use specified device: %s\n", pDeviceIndex); + device_index = atoi(pDeviceIndex); + } + + for (int32_t i = 0; i != num_devices; ++i) { + const PaDeviceInfo *info = Pa_GetDeviceInfo(i); + fprintf(stderr, " %s %d %s\n", (i == device_index) ? "*" : " ", i, + info->name); + } + + PaStreamParameters param; + param.device = device_index; + + fprintf(stderr, "Use device: %d\n", param.device); const PaDeviceInfo *info = Pa_GetDeviceInfo(param.device); fprintf(stderr, " Name: %s\n", info->name); @@ -241,12 +258,18 @@ Note that `zh` means Chinese, while `en` means English. param.suggestedLatency = info->defaultLowInputLatency; param.hostApiSpecificStreamInfo = nullptr; + float mic_sample_rate = 16000; + const char *pSampleRateStr = std::getenv("SHERPA_ONNX_MIC_SAMPLE_RATE"); + if (pSampleRateStr) { + fprintf(stderr, "Use sample rate %f for mic\n", mic_sample_rate); + mic_sample_rate = atof(pSampleRateStr); + } float sample_rate = 16000; PaStream *stream; PaError err = Pa_OpenStream(&stream, ¶m, nullptr, /* &outputParameters, */ - sample_rate, + mic_sample_rate, 0, // frames per buffer paClipOff, // we won't output out of range samples // so don't bother clipping them @@ -279,7 +302,7 @@ Note that `zh` means Chinese, while `en` means English. } auto s = extractor.CreateStream(); - s->AcceptWaveform(sample_rate, buf.data(), buf.size()); + s->AcceptWaveform(mic_sample_rate, buf.data(), buf.size()); s->InputFinished(); auto embedding = extractor.Compute(s.get()); auto name = manager.Search(embedding.data(), threshold); diff --git a/sherpa-onnx/csrc/sherpa-onnx-microphone-offline.cc b/sherpa-onnx/csrc/sherpa-onnx-microphone-offline.cc index a587ffa44..75ffa97f0 100644 --- a/sherpa-onnx/csrc/sherpa-onnx-microphone-offline.cc +++ b/sherpa-onnx/csrc/sherpa-onnx-microphone-offline.cc @@ -139,14 +139,31 @@ for a list of pre-trained models to download. PaDeviceIndex num_devices = Pa_GetDeviceCount(); fprintf(stderr, "Num devices: %d\n", num_devices); - PaStreamParameters param; + int32_t device_index = Pa_GetDefaultInputDevice(); - param.device = Pa_GetDefaultInputDevice(); - if (param.device == paNoDevice) { + if (device_index == paNoDevice) { fprintf(stderr, "No default input device found\n"); + fprintf(stderr, "If you are using Linux, please switch to \n"); + fprintf(stderr, " ./bin/sherpa-onnx-alsa-offline \n"); exit(EXIT_FAILURE); } - fprintf(stderr, "Use default device: %d\n", param.device); + + const char *pDeviceIndex = std::getenv("SHERPA_ONNX_MIC_DEVICE"); + if (pDeviceIndex) { + fprintf(stderr, "Use specified device: %s\n", pDeviceIndex); + device_index = atoi(pDeviceIndex); + } + + for (int32_t i = 0; i != num_devices; ++i) { + const PaDeviceInfo *info = Pa_GetDeviceInfo(i); + fprintf(stderr, " %s %d %s\n", (i == device_index) ? "*" : " ", i, + info->name); + } + + PaStreamParameters param; + param.device = device_index; + + fprintf(stderr, "Use device: %d\n", param.device); const PaDeviceInfo *info = Pa_GetDeviceInfo(param.device); fprintf(stderr, " Name: %s\n", info->name); @@ -157,12 +174,18 @@ for a list of pre-trained models to download. param.suggestedLatency = info->defaultLowInputLatency; param.hostApiSpecificStreamInfo = nullptr; + float mic_sample_rate = 16000; + const char *pSampleRateStr = std::getenv("SHERPA_ONNX_MIC_SAMPLE_RATE"); + if (pSampleRateStr) { + fprintf(stderr, "Use sample rate %f for mic\n", mic_sample_rate); + mic_sample_rate = atof(pSampleRateStr); + } float sample_rate = 16000; PaStream *stream; PaError err = Pa_OpenStream(&stream, ¶m, nullptr, /* &outputParameters, */ - sample_rate, + mic_sample_rate, 0, // frames per buffer paClipOff, // we won't output out of range samples // so don't bother clipping them @@ -195,7 +218,7 @@ for a list of pre-trained models to download. } auto s = recognizer.CreateStream(); - s->AcceptWaveform(sample_rate, buf.data(), buf.size()); + s->AcceptWaveform(mic_sample_rate, buf.data(), buf.size()); recognizer.DecodeStream(s.get()); SHERPA_ONNX_LOGE("Decoding Done! Result is:"); SHERPA_ONNX_LOGE("%s", s->GetResult().text.c_str()); diff --git a/sherpa-onnx/csrc/sherpa-onnx-microphone.cc b/sherpa-onnx/csrc/sherpa-onnx-microphone.cc index bdb43a204..cb8e4d8d9 100644 --- a/sherpa-onnx/csrc/sherpa-onnx-microphone.cc +++ b/sherpa-onnx/csrc/sherpa-onnx-microphone.cc @@ -15,6 +15,7 @@ #include "sherpa-onnx/csrc/online-recognizer.h" bool stop = false; +float mic_sample_rate = 16000; static int32_t RecordCallback(const void *input_buffer, void * /*output_buffer*/, @@ -24,7 +25,8 @@ static int32_t RecordCallback(const void *input_buffer, void *user_data) { auto stream = reinterpret_cast(user_data); - stream->AcceptWaveform(16000, reinterpret_cast(input_buffer), + stream->AcceptWaveform(mic_sample_rate, + reinterpret_cast(input_buffer), frames_per_buffer); return stop ? paComplete : paContinue; @@ -81,14 +83,31 @@ for a list of pre-trained models to download. PaDeviceIndex num_devices = Pa_GetDeviceCount(); fprintf(stderr, "Num devices: %d\n", num_devices); - PaStreamParameters param; + int32_t device_index = Pa_GetDefaultInputDevice(); - param.device = Pa_GetDefaultInputDevice(); - if (param.device == paNoDevice) { + if (device_index == paNoDevice) { fprintf(stderr, "No default input device found\n"); + fprintf(stderr, "If you are using Linux, please switch to \n"); + fprintf(stderr, " ./bin/sherpa-onnx-alsa \n"); exit(EXIT_FAILURE); } - fprintf(stderr, "Use default device: %d\n", param.device); + + const char *pDeviceIndex = std::getenv("SHERPA_ONNX_MIC_DEVICE"); + if (pDeviceIndex) { + fprintf(stderr, "Use specified device: %s\n", pDeviceIndex); + device_index = atoi(pDeviceIndex); + } + + for (int32_t i = 0; i != num_devices; ++i) { + const PaDeviceInfo *info = Pa_GetDeviceInfo(i); + fprintf(stderr, " %s %d %s\n", (i == device_index) ? "*" : " ", i, + info->name); + } + + PaStreamParameters param; + param.device = device_index; + + fprintf(stderr, "Use device: %d\n", param.device); const PaDeviceInfo *info = Pa_GetDeviceInfo(param.device); fprintf(stderr, " Name: %s\n", info->name); @@ -99,6 +118,11 @@ for a list of pre-trained models to download. param.suggestedLatency = info->defaultLowInputLatency; param.hostApiSpecificStreamInfo = nullptr; + const char *pSampleRateStr = std::getenv("SHERPA_ONNX_MIC_SAMPLE_RATE"); + if (pSampleRateStr) { + fprintf(stderr, "Use sample rate %f for mic\n", mic_sample_rate); + mic_sample_rate = atof(pSampleRateStr); + } float sample_rate = 16000; PaStream *stream; diff --git a/sherpa-onnx/csrc/sherpa-onnx-vad-alsa.cc b/sherpa-onnx/csrc/sherpa-onnx-vad-alsa.cc index 31a3f39b0..47fa6119d 100644 --- a/sherpa-onnx/csrc/sherpa-onnx-vad-alsa.cc +++ b/sherpa-onnx/csrc/sherpa-onnx-vad-alsa.cc @@ -47,7 +47,7 @@ card 3: UACDemoV10 [UACDemoV1.0], device 0: USB Audio [USB Audio] Subdevices: 1/1 Subdevice #0: subdevice #0 -and if you want to select card 3 and the device 0 on that card, please use: +and if you want to select card 3 and device 0 on that card, please use: plughw:3,0 diff --git a/sherpa-onnx/csrc/sherpa-onnx-vad-microphone-offline-asr.cc b/sherpa-onnx/csrc/sherpa-onnx-vad-microphone-offline-asr.cc index e7d8c0349..0632e81ae 100644 --- a/sherpa-onnx/csrc/sherpa-onnx-vad-microphone-offline-asr.cc +++ b/sherpa-onnx/csrc/sherpa-onnx-vad-microphone-offline-asr.cc @@ -13,6 +13,7 @@ #include "sherpa-onnx/csrc/circular-buffer.h" #include "sherpa-onnx/csrc/microphone.h" #include "sherpa-onnx/csrc/offline-recognizer.h" +#include "sherpa-onnx/csrc/resample.h" #include "sherpa-onnx/csrc/voice-activity-detector.h" bool stop = false; @@ -115,14 +116,29 @@ to download models for offline ASR. PaDeviceIndex num_devices = Pa_GetDeviceCount(); fprintf(stderr, "Num devices: %d\n", num_devices); - PaStreamParameters param; + int32_t device_index = Pa_GetDefaultInputDevice(); - param.device = Pa_GetDefaultInputDevice(); - if (param.device == paNoDevice) { + if (device_index == paNoDevice) { fprintf(stderr, "No default input device found\n"); exit(EXIT_FAILURE); } - fprintf(stderr, "Use default device: %d\n", param.device); + + const char *pDeviceIndex = std::getenv("SHERPA_ONNX_MIC_DEVICE"); + if (pDeviceIndex) { + fprintf(stderr, "Use specified device: %s\n", pDeviceIndex); + device_index = atoi(pDeviceIndex); + } + + for (int32_t i = 0; i != num_devices; ++i) { + const PaDeviceInfo *info = Pa_GetDeviceInfo(i); + fprintf(stderr, " %s %d %s\n", (i == device_index) ? "*" : " ", i, + info->name); + } + + PaStreamParameters param; + param.device = device_index; + + fprintf(stderr, "Use device: %d\n", param.device); const PaDeviceInfo *info = Pa_GetDeviceInfo(param.device); fprintf(stderr, " Name: %s\n", info->name); @@ -133,12 +149,27 @@ to download models for offline ASR. param.suggestedLatency = info->defaultLowInputLatency; param.hostApiSpecificStreamInfo = nullptr; + float mic_sample_rate = 16000; + const char *pSampleRateStr = std::getenv("SHERPA_ONNX_MIC_SAMPLE_RATE"); + if (pSampleRateStr) { + fprintf(stderr, "Use sample rate %f for mic\n", mic_sample_rate); + mic_sample_rate = atof(pSampleRateStr); + } float sample_rate = 16000; + std::unique_ptr resampler; + if (mic_sample_rate != sample_rate) { + float min_freq = std::min(mic_sample_rate, sample_rate); + float lowpass_cutoff = 0.99 * 0.5 * min_freq; + + int32_t lowpass_filter_width = 6; + resampler = std::make_unique( + mic_sample_rate, sample_rate, lowpass_cutoff, lowpass_filter_width); + } PaStream *stream; PaError err = Pa_OpenStream(&stream, ¶m, nullptr, /* &outputParameters, */ - sample_rate, + mic_sample_rate, 0, // frames per buffer paClipOff, // we won't output out of range samples // so don't bother clipping them @@ -168,6 +199,13 @@ to download models for offline ASR. while (buffer.Size() >= window_size) { std::vector samples = buffer.Get(buffer.Head(), window_size); buffer.Pop(window_size); + + if (resampler) { + std::vector tmp; + resampler->Resample(samples.data(), samples.size(), true, &tmp); + samples = std::move(tmp); + } + vad->AcceptWaveform(samples.data(), samples.size()); } } diff --git a/sherpa-onnx/csrc/sherpa-onnx-vad-microphone.cc b/sherpa-onnx/csrc/sherpa-onnx-vad-microphone.cc index da013b9e8..bf22f1693 100644 --- a/sherpa-onnx/csrc/sherpa-onnx-vad-microphone.cc +++ b/sherpa-onnx/csrc/sherpa-onnx-vad-microphone.cc @@ -12,6 +12,7 @@ #include "portaudio.h" // NOLINT #include "sherpa-onnx/csrc/circular-buffer.h" #include "sherpa-onnx/csrc/microphone.h" +#include "sherpa-onnx/csrc/resample.h" #include "sherpa-onnx/csrc/voice-activity-detector.h" #include "sherpa-onnx/csrc/wave-writer.h" @@ -76,14 +77,31 @@ wget https://github.com/snakers4/silero-vad/raw/master/files/silero_vad.onnx PaDeviceIndex num_devices = Pa_GetDeviceCount(); fprintf(stderr, "Num devices: %d\n", num_devices); - PaStreamParameters param; + int32_t device_index = Pa_GetDefaultInputDevice(); - param.device = Pa_GetDefaultInputDevice(); - if (param.device == paNoDevice) { + if (device_index == paNoDevice) { fprintf(stderr, "No default input device found\n"); + fprintf(stderr, "If you are using Linux, please switch to \n"); + fprintf(stderr, " ./bin/sherpa-onnx-vad-alsa \n"); exit(EXIT_FAILURE); } - fprintf(stderr, "Use default device: %d\n", param.device); + + const char *pDeviceIndex = std::getenv("SHERPA_ONNX_MIC_DEVICE"); + if (pDeviceIndex) { + fprintf(stderr, "Use specified device: %s\n", pDeviceIndex); + device_index = atoi(pDeviceIndex); + } + + for (int32_t i = 0; i != num_devices; ++i) { + const PaDeviceInfo *info = Pa_GetDeviceInfo(i); + fprintf(stderr, " %s %d %s\n", (i == device_index) ? "*" : " ", i, + info->name); + } + + PaStreamParameters param; + param.device = device_index; + + fprintf(stderr, "Use device: %d\n", param.device); const PaDeviceInfo *info = Pa_GetDeviceInfo(param.device); fprintf(stderr, " Name: %s\n", info->name); @@ -94,12 +112,28 @@ wget https://github.com/snakers4/silero-vad/raw/master/files/silero_vad.onnx param.suggestedLatency = info->defaultLowInputLatency; param.hostApiSpecificStreamInfo = nullptr; + float mic_sample_rate = 16000; + const char *pSampleRateStr = std::getenv("SHERPA_ONNX_MIC_SAMPLE_RATE"); + if (pSampleRateStr) { + fprintf(stderr, "Use sample rate %f for mic\n", mic_sample_rate); + mic_sample_rate = atof(pSampleRateStr); + } float sample_rate = 16000; + std::unique_ptr resampler; + if (mic_sample_rate != sample_rate) { + float min_freq = std::min(mic_sample_rate, sample_rate); + float lowpass_cutoff = 0.99 * 0.5 * min_freq; + + int32_t lowpass_filter_width = 6; + resampler = std::make_unique( + mic_sample_rate, sample_rate, lowpass_cutoff, lowpass_filter_width); + } + PaStream *stream; PaError err = Pa_OpenStream(&stream, ¶m, nullptr, /* &outputParameters, */ - sample_rate, + mic_sample_rate, 0, // frames per buffer paClipOff, // we won't output out of range samples // so don't bother clipping them @@ -131,6 +165,13 @@ wget https://github.com/snakers4/silero-vad/raw/master/files/silero_vad.onnx while (buffer.Size() >= window_size) { std::vector samples = buffer.Get(buffer.Head(), window_size); buffer.Pop(window_size); + + if (resampler) { + std::vector tmp; + resampler->Resample(samples.data(), samples.size(), true, &tmp); + samples = std::move(tmp); + } + vad->AcceptWaveform(samples.data(), samples.size()); if (vad->IsSpeechDetected() && !printed) { @@ -149,7 +190,7 @@ wget https://github.com/snakers4/silero-vad/raw/master/files/silero_vad.onnx char filename[128]; snprintf(filename, sizeof(filename), "seg-%d-%.3fs.wav", k, duration); k += 1; - sherpa_onnx::WriteWave(filename, 16000, segment.samples.data(), + sherpa_onnx::WriteWave(filename, sample_rate, segment.samples.data(), segment.samples.size()); fprintf(stderr, "Saved to %s\n", filename); fprintf(stderr, "----------\n"); From d21c45d0ea72da36bf02d20f085f1addd1baf84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=83=E5=AE=9D?= Date: Thu, 11 Apr 2024 09:07:31 +0800 Subject: [PATCH 13/45] Add --continue to wget (#750) Also, switch to github mirror --- build-ios.sh | 10 ++++++++-- cmake/asio.cmake | 2 +- cmake/cargs.cmake | 2 +- cmake/googletest.cmake | 2 +- cmake/kaldi-native-fbank.cmake | 2 +- cmake/onnxruntime-linux-x86_64.cmake | 2 +- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/build-ios.sh b/build-ios.sh index a687dc4de..265f50645 100755 --- a/build-ios.sh +++ b/build-ios.sh @@ -8,11 +8,17 @@ cd $dir onnxruntime_version=1.17.1 onnxruntime_dir=ios-onnxruntime/$onnxruntime_version +SHERPA_ONNX_GITHUB=github.com + +if [ "$SHERPA_ONNX_GITHUB_MIRROW" == true ]; then + SHERPA_ONNX_GITHUB=hub.nuaa.cf +fi + if [ ! -f $onnxruntime_dir/onnxruntime.xcframework/ios-arm64/onnxruntime.a ]; then mkdir -p $onnxruntime_dir pushd $onnxruntime_dir - rm -f onnxruntime.xcframework-${onnxruntime_version}.tar.bz2 - wget https://github.com/csukuangfj/onnxruntime-libs/releases/download/v${onnxruntime_version}/onnxruntime.xcframework-${onnxruntime_version}.tar.bz2 +# rm -f onnxruntime.xcframework-${onnxruntime_version}.tar.bz2 + wget -c https://${SHERPA_ONNX_GITHUB}/csukuangfj/onnxruntime-libs/releases/download/v${onnxruntime_version}/onnxruntime.xcframework-${onnxruntime_version}.tar.bz2 tar xvf onnxruntime.xcframework-${onnxruntime_version}.tar.bz2 rm onnxruntime.xcframework-${onnxruntime_version}.tar.bz2 cd .. diff --git a/cmake/asio.cmake b/cmake/asio.cmake index 31af3a6b5..eaa262acb 100644 --- a/cmake/asio.cmake +++ b/cmake/asio.cmake @@ -2,7 +2,7 @@ function(download_asio) include(FetchContent) set(asio_URL "https://github.com/chriskohlhoff/asio/archive/refs/tags/asio-1-24-0.tar.gz") - set(asio_URL2 "https://huggingface.co/csukuangfj/sherpa-onnx-cmake-deps/resolve/main/asio-asio-1-24-0.tar.gz") + set(asio_URL2 "https://hub.nuaa.cf/chriskohlhoff/asio/archive/refs/tags/asio-1-24-0.tar.gz") set(asio_HASH "SHA256=cbcaaba0f66722787b1a7c33afe1befb3a012b5af3ad7da7ff0f6b8c9b7a8a5b") # If you don't have access to the Internet, diff --git a/cmake/cargs.cmake b/cmake/cargs.cmake index cdf9f56e7..54487a6f0 100644 --- a/cmake/cargs.cmake +++ b/cmake/cargs.cmake @@ -2,7 +2,7 @@ function(download_cargs) include(FetchContent) set(cargs_URL "https://github.com/likle/cargs/archive/refs/tags/v1.0.3.tar.gz") - set(cargs_URL2 "https://huggingface.co/csukuangfj/sherpa-onnx-cmake-deps/resolve/main/cargs-1.0.3.tar.gz") + set(cargs_URL2 "https://hub.nuaa.cf/likle/cargs/archive/refs/tags/v1.0.3.tar.gz") set(cargs_HASH "SHA256=ddba25bd35e9c6c75bc706c126001b8ce8e084d40ef37050e6aa6963e836eb8b") # If you don't have access to the Internet, diff --git a/cmake/googletest.cmake b/cmake/googletest.cmake index 534c50f8c..cf5fa10cc 100644 --- a/cmake/googletest.cmake +++ b/cmake/googletest.cmake @@ -2,7 +2,7 @@ function(download_googltest) include(FetchContent) set(googletest_URL "https://github.com/google/googletest/archive/refs/tags/v1.13.0.tar.gz") - set(googletest_URL2 "https://huggingface.co/csukuangfj/sherpa-onnx-cmake-deps/resolve/main/googletest-1.13.0.tar.gz") + set(googletest_URL2 "https://hub.nuaa.cf/google/googletest/archive/refs/tags/v1.13.0.tar.gz") set(googletest_HASH "SHA256=ad7fdba11ea011c1d925b3289cf4af2c66a352e18d4c7264392fead75e919363") # If you don't have access to the Internet, diff --git a/cmake/kaldi-native-fbank.cmake b/cmake/kaldi-native-fbank.cmake index ae478fafb..c77ec5fc6 100644 --- a/cmake/kaldi-native-fbank.cmake +++ b/cmake/kaldi-native-fbank.cmake @@ -3,7 +3,7 @@ function(download_kaldi_native_fbank) set(kaldi_native_fbank_URL "https://github.com/csukuangfj/kaldi-native-fbank/archive/refs/tags/v1.19.1.tar.gz") set(kaldi_native_fbank_URL2 "https://hub.nuaa.cf/csukuangfj/kaldi-native-fbank/archive/refs/tags/v1.19.1.tar.gz") - set(kaldi_native_fbank_URL2 "https://huggingface.co/csukuangfj/sherpa-onnx-cmake-deps/resolve/main/kaldi-native-fbank-1.19.1.tar.gz") +# set(kaldi_native_fbank_URL2 "https://huggingface.co/csukuangfj/sherpa-onnx-cmake-deps/resolve/main/kaldi-native-fbank-1.19.1.tar.gz") set(kaldi_native_fbank_HASH "SHA256=0cae8cbb9ea42916b214e088912f9e8f2f648f54756b305f93f552382f31f904") set(KALDI_NATIVE_FBANK_BUILD_TESTS OFF CACHE BOOL "" FORCE) diff --git a/cmake/onnxruntime-linux-x86_64.cmake b/cmake/onnxruntime-linux-x86_64.cmake index 87e4268fe..5cd3fb83b 100644 --- a/cmake/onnxruntime-linux-x86_64.cmake +++ b/cmake/onnxruntime-linux-x86_64.cmake @@ -15,7 +15,7 @@ if(NOT BUILD_SHARED_LIBS) endif() set(onnxruntime_URL "https://github.com/csukuangfj/onnxruntime-libs/releases/download/v1.17.1/onnxruntime-linux-x64-glibc2_17-Release-1.17.1.zip") -set(onnxruntime_URL2 "https://github.com/csukuangfj/onnxruntime-libs/releases/download/v1.17.1/onnxruntime-linux-x64-glibc2_17-Release-1.17.1.zip") +set(onnxruntime_URL2 "https://hub.nuaa.cf/csukuangfj/onnxruntime-libs/releases/download/v1.17.1/onnxruntime-linux-x64-glibc2_17-Release-1.17.1.zip") set(onnxruntime_HASH "SHA256=3cfa5c2c5c21a9401572af5a4cd9d15ed8f6524f10d3b80e5a38676b3a31efe0") # If you don't have access to the Internet, From 904a3cc8a9fb396be39a456392b79ebef2af50de Mon Sep 17 00:00:00 2001 From: AHN Sung Hwan Date: Thu, 11 Apr 2024 11:34:44 +0900 Subject: [PATCH 14/45] Fix a bug in mean calculation of 'ys_probs' (#748) --- sherpa-onnx/csrc/transducer-keyword-decoder.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sherpa-onnx/csrc/transducer-keyword-decoder.cc b/sherpa-onnx/csrc/transducer-keyword-decoder.cc index f31348ea9..af78cb9c2 100644 --- a/sherpa-onnx/csrc/transducer-keyword-decoder.cc +++ b/sherpa-onnx/csrc/transducer-keyword-decoder.cc @@ -152,7 +152,7 @@ void TransducerKeywordDecoder::Decode( if (matched) { float ys_prob = 0.0; int32_t length = best_hyp.ys_probs.size(); - for (int32_t i = 1; i <= matched_state->level; ++i) { + for (int32_t i = 0; i < matched_state->level; ++i) { ys_prob += best_hyp.ys_probs[i]; } ys_prob /= matched_state->level; From 34d70a259fb1f67fd09b64bb9302f3b86f7be388 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Thu, 11 Apr 2024 11:12:48 +0800 Subject: [PATCH 15/45] Add Python API and Python examples for audio tagging (#753) --- .github/scripts/test-python.sh | 9 ++ build-ios.sh | 1 - cmake/kaldi-native-fbank.cmake | 1 - .../audio-tagging-from-a-file.py | 121 ++++++++++++++++++ sherpa-onnx/python/csrc/CMakeLists.txt | 1 + sherpa-onnx/python/csrc/audio-tagging.cc | 87 +++++++++++++ sherpa-onnx/python/csrc/audio-tagging.h | 16 +++ .../csrc/offline-tts-vits-model-config.cc | 2 +- sherpa-onnx/python/csrc/sherpa-onnx.cc | 2 + .../csrc/speaker-embedding-extractor.cc | 2 +- .../csrc/spoken-language-identification.cc | 4 +- sherpa-onnx/python/sherpa_onnx/__init__.py | 5 + 12 files changed, 245 insertions(+), 6 deletions(-) create mode 100755 python-api-examples/audio-tagging-from-a-file.py create mode 100644 sherpa-onnx/python/csrc/audio-tagging.cc create mode 100644 sherpa-onnx/python/csrc/audio-tagging.h diff --git a/.github/scripts/test-python.sh b/.github/scripts/test-python.sh index 3604a0059..aa9b795f1 100755 --- a/.github/scripts/test-python.sh +++ b/.github/scripts/test-python.sh @@ -8,6 +8,15 @@ log() { echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" } +log "test audio tagging" + +curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/audio-tagging-models/sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 +tar xvf sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 +rm sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 + python3 ./python-api-examples/audio-tagging-from-a-file.py +rm -rf sherpa-onnx-zipformer-audio-tagging-2024-04-09 + + log "test streaming zipformer2 ctc HLG decoding" curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 diff --git a/build-ios.sh b/build-ios.sh index 265f50645..ac81b4fb1 100755 --- a/build-ios.sh +++ b/build-ios.sh @@ -17,7 +17,6 @@ fi if [ ! -f $onnxruntime_dir/onnxruntime.xcframework/ios-arm64/onnxruntime.a ]; then mkdir -p $onnxruntime_dir pushd $onnxruntime_dir -# rm -f onnxruntime.xcframework-${onnxruntime_version}.tar.bz2 wget -c https://${SHERPA_ONNX_GITHUB}/csukuangfj/onnxruntime-libs/releases/download/v${onnxruntime_version}/onnxruntime.xcframework-${onnxruntime_version}.tar.bz2 tar xvf onnxruntime.xcframework-${onnxruntime_version}.tar.bz2 rm onnxruntime.xcframework-${onnxruntime_version}.tar.bz2 diff --git a/cmake/kaldi-native-fbank.cmake b/cmake/kaldi-native-fbank.cmake index c77ec5fc6..ce76745ed 100644 --- a/cmake/kaldi-native-fbank.cmake +++ b/cmake/kaldi-native-fbank.cmake @@ -3,7 +3,6 @@ function(download_kaldi_native_fbank) set(kaldi_native_fbank_URL "https://github.com/csukuangfj/kaldi-native-fbank/archive/refs/tags/v1.19.1.tar.gz") set(kaldi_native_fbank_URL2 "https://hub.nuaa.cf/csukuangfj/kaldi-native-fbank/archive/refs/tags/v1.19.1.tar.gz") -# set(kaldi_native_fbank_URL2 "https://huggingface.co/csukuangfj/sherpa-onnx-cmake-deps/resolve/main/kaldi-native-fbank-1.19.1.tar.gz") set(kaldi_native_fbank_HASH "SHA256=0cae8cbb9ea42916b214e088912f9e8f2f648f54756b305f93f552382f31f904") set(KALDI_NATIVE_FBANK_BUILD_TESTS OFF CACHE BOOL "" FORCE) diff --git a/python-api-examples/audio-tagging-from-a-file.py b/python-api-examples/audio-tagging-from-a-file.py new file mode 100755 index 000000000..e5cb9feb7 --- /dev/null +++ b/python-api-examples/audio-tagging-from-a-file.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 + +""" +This script shows how to use audio tagging Python APIs to tag a file. + +Please read the code to download the required model files and test wave file. +""" + +import logging +import time +from pathlib import Path + +import numpy as np +import sherpa_onnx +import soundfile as sf + + +def read_test_wave(): + # Please download the model files and test wave files from + # https://github.com/k2-fsa/sherpa-onnx/releases/tag/audio-tagging-models + test_wave = "./sherpa-onnx-zipformer-audio-tagging-2024-04-09/test_wavs/1.wav" + + if not Path(test_wave).is_file(): + raise ValueError( + f"Please download {test_wave} from " + "https://github.com/k2-fsa/sherpa-onnx/releases/tag/audio-tagging-models" + ) + + # See https://python-soundfile.readthedocs.io/en/0.11.0/#soundfile.read + data, sample_rate = sf.read( + test_wave, + always_2d=True, + dtype="float32", + ) + data = data[:, 0] # use only the first channel + samples = np.ascontiguousarray(data) + + # samples is a 1-d array of dtype float32 + # sample_rate is a scalar + return samples, sample_rate + + +def create_audio_tagger(): + # Please download the model files and test wave files from + # https://github.com/k2-fsa/sherpa-onnx/releases/tag/audio-tagging-models + model_file = "./sherpa-onnx-zipformer-audio-tagging-2024-04-09/model.onnx" + label_file = ( + "./sherpa-onnx-zipformer-audio-tagging-2024-04-09/class_labels_indices.csv" + ) + + if not Path(model_file).is_file(): + raise ValueError( + f"Please download {model_file} from " + "https://github.com/k2-fsa/sherpa-onnx/releases/tag/audio-tagging-models" + ) + + if not Path(label_file).is_file(): + raise ValueError( + f"Please download {label_file} from " + "https://github.com/k2-fsa/sherpa-onnx/releases/tag/audio-tagging-models" + ) + + config = sherpa_onnx.AudioTaggingConfig( + model=sherpa_onnx.AudioTaggingModelConfig( + zipformer=sherpa_onnx.OfflineZipformerAudioTaggingModelConfig( + model=model_file, + ), + num_threads=1, + debug=True, + provider="cpu", + ), + labels=label_file, + top_k=5, + ) + if not config.validate(): + raise ValueError(f"Please check the config: {config}") + + print(config) + + return sherpa_onnx.AudioTagging(config) + + +def main(): + logging.info("Create audio tagger") + audio_tagger = create_audio_tagger() + + logging.info("Read test wave") + samples, sample_rate = read_test_wave() + + logging.info("Computing") + + start_time = time.time() + + stream = audio_tagger.create_stream() + stream.accept_waveform(sample_rate=sample_rate, waveform=samples) + result = audio_tagger.compute(stream) + end_time = time.time() + + elapsed_seconds = end_time - start_time + audio_duration = len(samples) / sample_rate + + real_time_factor = elapsed_seconds / audio_duration + logging.info(f"Elapsed seconds: {elapsed_seconds:.3f}") + logging.info(f"Audio duration in seconds: {audio_duration:.3f}") + logging.info( + f"RTF: {elapsed_seconds:.3f}/{audio_duration:.3f} = {real_time_factor:.3f}" + ) + + s = "\n" + for i, e in enumerate(result): + s += f"{i}: {e}\n" + + logging.info(s) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + + main() diff --git a/sherpa-onnx/python/csrc/CMakeLists.txt b/sherpa-onnx/python/csrc/CMakeLists.txt index 12409a9be..266b7c312 100644 --- a/sherpa-onnx/python/csrc/CMakeLists.txt +++ b/sherpa-onnx/python/csrc/CMakeLists.txt @@ -1,6 +1,7 @@ include_directories(${CMAKE_SOURCE_DIR}) set(srcs + audio-tagging.cc circular-buffer.cc display.cc endpoint.cc diff --git a/sherpa-onnx/python/csrc/audio-tagging.cc b/sherpa-onnx/python/csrc/audio-tagging.cc new file mode 100644 index 000000000..170bbc6c2 --- /dev/null +++ b/sherpa-onnx/python/csrc/audio-tagging.cc @@ -0,0 +1,87 @@ +// sherpa-onnx/python/csrc/audio-tagging.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/python/csrc/audio-tagging.h" + +#include + +#include "sherpa-onnx/csrc/audio-tagging.h" + +namespace sherpa_onnx { + +static void PybindOfflineZipformerAudioTaggingModelConfig(py::module *m) { + using PyClass = OfflineZipformerAudioTaggingModelConfig; + py::class_(*m, "OfflineZipformerAudioTaggingModelConfig") + .def(py::init<>()) + .def(py::init(), py::arg("model")) + .def_readwrite("model", &PyClass::model) + .def("validate", &PyClass::Validate) + .def("__str__", &PyClass::ToString); +} + +static void PybindAudioTaggingModelConfig(py::module *m) { + PybindOfflineZipformerAudioTaggingModelConfig(m); + + using PyClass = AudioTaggingModelConfig; + + py::class_(*m, "AudioTaggingModelConfig") + .def(py::init<>()) + .def(py::init(), + py::arg("zipformer"), py::arg("num_threads") = 1, + py::arg("debug") = false, py::arg("provider") = "cpu") + .def_readwrite("zipformer", &PyClass::zipformer) + .def_readwrite("num_threads", &PyClass::num_threads) + .def_readwrite("debug", &PyClass::debug) + .def_readwrite("provider", &PyClass::provider) + .def("validate", &PyClass::Validate) + .def("__str__", &PyClass::ToString); +} + +static void PybindAudioTaggingConfig(py::module *m) { + PybindAudioTaggingModelConfig(m); + + using PyClass = AudioTaggingConfig; + + py::class_(*m, "AudioTaggingConfig") + .def(py::init<>()) + .def(py::init(), + py::arg("model"), py::arg("labels"), py::arg("top_k") = 5) + .def_readwrite("model", &PyClass::model) + .def_readwrite("labels", &PyClass::labels) + .def_readwrite("top_k", &PyClass::top_k) + .def("validate", &PyClass::Validate) + .def("__str__", &PyClass::ToString); +} + +static void PybindAudioEvent(py::module *m) { + using PyClass = AudioEvent; + + py::class_(*m, "AudioEvent") + .def_property_readonly( + "name", [](const PyClass &self) -> std::string { return self.name; }) + .def_property_readonly( + "index", [](const PyClass &self) -> int32_t { return self.index; }) + .def_property_readonly( + "prob", [](const PyClass &self) -> float { return self.prob; }) + .def("__str__", &PyClass::ToString); +} + +void PybindAudioTagging(py::module *m) { + PybindAudioTaggingConfig(m); + PybindAudioEvent(m); + + using PyClass = AudioTagging; + + py::class_(*m, "AudioTagging") + .def(py::init(), py::arg("config"), + py::call_guard()) + .def("create_stream", &PyClass::CreateStream, + py::call_guard()) + .def("compute", &PyClass::Compute, py::arg("s"), py::arg("top_k") = -1, + py::call_guard()); +} + +} // namespace sherpa_onnx diff --git a/sherpa-onnx/python/csrc/audio-tagging.h b/sherpa-onnx/python/csrc/audio-tagging.h new file mode 100644 index 000000000..1cf3eaefb --- /dev/null +++ b/sherpa-onnx/python/csrc/audio-tagging.h @@ -0,0 +1,16 @@ +// sherpa-onnx/python/csrc/audio-tagging.h +// +// Copyright (c) 2024 Xiaomi Corporation + +#ifndef SHERPA_ONNX_PYTHON_CSRC_AUDIO_TAGGING_H_ +#define SHERPA_ONNX_PYTHON_CSRC_AUDIO_TAGGING_H_ + +#include "sherpa-onnx/python/csrc/sherpa-onnx.h" + +namespace sherpa_onnx { + +void PybindAudioTagging(py::module *m); + +} + +#endif // SHERPA_ONNX_PYTHON_CSRC_AUDIO_TAGGING_H_ diff --git a/sherpa-onnx/python/csrc/offline-tts-vits-model-config.cc b/sherpa-onnx/python/csrc/offline-tts-vits-model-config.cc index 6e016715d..c88c92e0b 100644 --- a/sherpa-onnx/python/csrc/offline-tts-vits-model-config.cc +++ b/sherpa-onnx/python/csrc/offline-tts-vits-model-config.cc @@ -16,7 +16,7 @@ void PybindOfflineTtsVitsModelConfig(py::module *m) { py::class_(*m, "OfflineTtsVitsModelConfig") .def(py::init<>()) .def(py::init(), py::arg("model"), py::arg("lexicon"), py::arg("tokens"), py::arg("data_dir") = "", py::arg("noise_scale") = 0.667, diff --git a/sherpa-onnx/python/csrc/sherpa-onnx.cc b/sherpa-onnx/python/csrc/sherpa-onnx.cc index 8a5ae5cd3..31dd9bafd 100644 --- a/sherpa-onnx/python/csrc/sherpa-onnx.cc +++ b/sherpa-onnx/python/csrc/sherpa-onnx.cc @@ -5,6 +5,7 @@ #include "sherpa-onnx/python/csrc/sherpa-onnx.h" #include "sherpa-onnx/python/csrc/alsa.h" +#include "sherpa-onnx/python/csrc/audio-tagging.h" #include "sherpa-onnx/python/csrc/circular-buffer.h" #include "sherpa-onnx/python/csrc/display.h" #include "sherpa-onnx/python/csrc/endpoint.h" @@ -38,6 +39,7 @@ PYBIND11_MODULE(_sherpa_onnx, m) { m.doc() = "pybind11 binding of sherpa-onnx"; PybindWaveWriter(&m); + PybindAudioTagging(&m); PybindFeatures(&m); PybindOnlineCtcFstDecoderConfig(&m); diff --git a/sherpa-onnx/python/csrc/speaker-embedding-extractor.cc b/sherpa-onnx/python/csrc/speaker-embedding-extractor.cc index 2749ba3bd..e5703caa6 100644 --- a/sherpa-onnx/python/csrc/speaker-embedding-extractor.cc +++ b/sherpa-onnx/python/csrc/speaker-embedding-extractor.cc @@ -14,7 +14,7 @@ static void PybindSpeakerEmbeddingExtractorConfig(py::module *m) { using PyClass = SpeakerEmbeddingExtractorConfig; py::class_(*m, "SpeakerEmbeddingExtractorConfig") .def(py::init<>()) - .def(py::init(), + .def(py::init(), py::arg("model"), py::arg("num_threads") = 1, py::arg("debug") = false, py::arg("provider") = "cpu") .def_readwrite("model", &PyClass::model) diff --git a/sherpa-onnx/python/csrc/spoken-language-identification.cc b/sherpa-onnx/python/csrc/spoken-language-identification.cc index f528e5561..b49f9a9bc 100644 --- a/sherpa-onnx/python/csrc/spoken-language-identification.cc +++ b/sherpa-onnx/python/csrc/spoken-language-identification.cc @@ -33,7 +33,7 @@ static void PybindSpokenLanguageIdentificationConfig(py::module *m) { py::class_(*m, "SpokenLanguageIdentificationConfig") .def(py::init<>()) .def(py::init(), + bool, const std::string &>(), py::arg("whisper"), py::arg("num_threads") = 1, py::arg("debug") = false, py::arg("provider") = "cpu") .def_readwrite("whisper", &PyClass::whisper) @@ -53,7 +53,7 @@ void PybindSpokenLanguageIdentification(py::module *m) { py::arg("config"), py::call_guard()) .def("create_stream", &PyClass::CreateStream, py::call_guard()) - .def("compute", &PyClass::Compute, + .def("compute", &PyClass::Compute, py::arg("s"), py::call_guard()); } diff --git a/sherpa-onnx/python/sherpa_onnx/__init__.py b/sherpa-onnx/python/sherpa_onnx/__init__.py index 2282687ea..2b1607312 100644 --- a/sherpa-onnx/python/sherpa_onnx/__init__.py +++ b/sherpa-onnx/python/sherpa_onnx/__init__.py @@ -1,5 +1,9 @@ from _sherpa_onnx import ( Alsa, + AudioEvent, + AudioTagging, + AudioTaggingConfig, + AudioTaggingModelConfig, CircularBuffer, Display, OfflineStream, @@ -7,6 +11,7 @@ OfflineTtsConfig, OfflineTtsModelConfig, OfflineTtsVitsModelConfig, + OfflineZipformerAudioTaggingModelConfig, OnlineStream, SileroVadModelConfig, SpeakerEmbeddingExtractor, From f204e62b44147c8eb18a5a00d6043e8d1d70e6d7 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Thu, 11 Apr 2024 14:18:43 +0800 Subject: [PATCH 16/45] Add C API for audio tagging (#754) --- .github/scripts/test-c-api.sh | 13 ++++ .github/workflows/linux.yaml | 18 ++--- .github/workflows/macos.yaml | 15 ++-- .github/workflows/windows-x64.yaml | 16 +++-- .github/workflows/windows-x86.yaml | 2 + c-api-examples/CMakeLists.txt | 3 + c-api-examples/audio-tagging-c-api.c | 79 +++++++++++++++++++++ sherpa-onnx/c-api/c-api.cc | 100 +++++++++++++++++++++++++-- sherpa-onnx/c-api/c-api.h | 71 +++++++++++++++++-- 9 files changed, 285 insertions(+), 32 deletions(-) create mode 100644 c-api-examples/audio-tagging-c-api.c diff --git a/.github/scripts/test-c-api.sh b/.github/scripts/test-c-api.sh index afc66c106..b29d1a0b4 100755 --- a/.github/scripts/test-c-api.sh +++ b/.github/scripts/test-c-api.sh @@ -10,8 +10,21 @@ log() { echo "SLID_EXE is $SLID_EXE" echo "SID_EXE is $SID_EXE" +echo "AT_EXE is $AT_EXE" echo "PATH: $PATH" +log "------------------------------------------------------------" +log "Test audio tagging " +log "------------------------------------------------------------" + +curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/audio-tagging-models/sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 +tar xvf sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 +rm sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 + +$AT_EXE + +rm -rf sherpa-onnx-zipformer-audio-tagging-2024-04-09 + log "------------------------------------------------------------" log "Download whisper tiny for spoken language identification " diff --git a/.github/workflows/linux.yaml b/.github/workflows/linux.yaml index ae0aec470..ee58cc408 100644 --- a/.github/workflows/linux.yaml +++ b/.github/workflows/linux.yaml @@ -126,6 +126,16 @@ jobs: name: release-${{ matrix.build_type }}-with-shared-lib-${{ matrix.shared_lib }}-with-tts-${{ matrix.with_tts }} path: build/bin/* + - name: Test C API + shell: bash + run: | + export PATH=$PWD/build/bin:$PATH + export SLID_EXE=spoken-language-identification-c-api + export SID_EXE=speaker-identification-c-api + export AT_EXE=audio-tagging-c-api + + .github/scripts/test-c-api.sh + - name: Test Audio tagging shell: bash run: | @@ -142,14 +152,6 @@ jobs: .github/scripts/test-online-ctc.sh - - name: Test C API - shell: bash - run: | - export PATH=$PWD/build/bin:$PATH - export SLID_EXE=spoken-language-identification-c-api - export SID_EXE=speaker-identification-c-api - - .github/scripts/test-c-api.sh - name: Test spoken language identification (C++ API) shell: bash diff --git a/.github/workflows/macos.yaml b/.github/workflows/macos.yaml index 9dfcb7c9d..99b4f301a 100644 --- a/.github/workflows/macos.yaml +++ b/.github/workflows/macos.yaml @@ -105,22 +105,23 @@ jobs: otool -L build/bin/sherpa-onnx otool -l build/bin/sherpa-onnx - - name: Test Audio tagging + - name: Test C API shell: bash run: | export PATH=$PWD/build/bin:$PATH - export EXE=sherpa-onnx-offline-audio-tagging + export SLID_EXE=spoken-language-identification-c-api + export SID_EXE=speaker-identification-c-api + export AT_EXE=audio-tagging-c-api - .github/scripts/test-audio-tagging.sh + .github/scripts/test-c-api.sh - - name: Test C API + - name: Test Audio tagging shell: bash run: | export PATH=$PWD/build/bin:$PATH - export SLID_EXE=spoken-language-identification-c-api - export SID_EXE=speaker-identification-c-api + export EXE=sherpa-onnx-offline-audio-tagging - .github/scripts/test-c-api.sh + .github/scripts/test-audio-tagging.sh - name: Test spoken language identification (C++ API) shell: bash diff --git a/.github/workflows/windows-x64.yaml b/.github/workflows/windows-x64.yaml index 8f1715591..cf000be8f 100644 --- a/.github/workflows/windows-x64.yaml +++ b/.github/workflows/windows-x64.yaml @@ -72,22 +72,24 @@ jobs: ls -lh ./bin/Release/sherpa-onnx.exe - - name: Test Audio tagging + - name: Test C API shell: bash run: | export PATH=$PWD/build/bin/Release:$PATH - export EXE=sherpa-onnx-offline-audio-tagging.exe + export SLID_EXE=spoken-language-identification-c-api.exe + export SID_EXE=speaker-identification-c-api.exe + export AT_EXE=audio-tagging-c-api.exe - .github/scripts/test-audio-tagging.sh + .github/scripts/test-c-api.sh - - name: Test C API + + - name: Test Audio tagging shell: bash run: | export PATH=$PWD/build/bin/Release:$PATH - export SLID_EXE=spoken-language-identification-c-api.exe - export SID_EXE=speaker-identification-c-api.exe + export EXE=sherpa-onnx-offline-audio-tagging.exe - .github/scripts/test-c-api.sh + .github/scripts/test-audio-tagging.sh - name: Test spoken language identification (C++ API) shell: bash diff --git a/.github/workflows/windows-x86.yaml b/.github/workflows/windows-x86.yaml index 65d1bea62..7a18e0be1 100644 --- a/.github/workflows/windows-x86.yaml +++ b/.github/workflows/windows-x86.yaml @@ -77,6 +77,8 @@ jobs: run: | export PATH=$PWD/build/bin/Release:$PATH export SLID_EXE=spoken-language-identification-c-api.exe + export SID_EXE=speaker-identification-c-api.exe + export AT_EXE=audio-tagging-c-api.exe .github/scripts/test-c-api.sh diff --git a/c-api-examples/CMakeLists.txt b/c-api-examples/CMakeLists.txt index 4c3669d1f..8d9bfe985 100644 --- a/c-api-examples/CMakeLists.txt +++ b/c-api-examples/CMakeLists.txt @@ -18,6 +18,9 @@ target_link_libraries(speaker-identification-c-api sherpa-onnx-c-api) add_executable(streaming-hlg-decode-file-c-api streaming-hlg-decode-file-c-api.c) target_link_libraries(streaming-hlg-decode-file-c-api sherpa-onnx-c-api) +add_executable(audio-tagging-c-api audio-tagging-c-api.c) +target_link_libraries(audio-tagging-c-api sherpa-onnx-c-api) + if(SHERPA_ONNX_HAS_ALSA) add_subdirectory(./asr-microphone-example) elseif((UNIX AND NOT APPLE) OR LINUX) diff --git a/c-api-examples/audio-tagging-c-api.c b/c-api-examples/audio-tagging-c-api.c new file mode 100644 index 000000000..1272717a5 --- /dev/null +++ b/c-api-examples/audio-tagging-c-api.c @@ -0,0 +1,79 @@ +// c-api-examples/audio-tagging-c-api.c +// +// Copyright (c) 2024 Xiaomi Corporation + +// We assume you have pre-downloaded the model files for testing +// from https://github.com/k2-fsa/sherpa-onnx/releases/tag/audio-tagging-models +// +// An example is given below: +// +// clang-format off +// +// wget https://github.com/k2-fsa/sherpa-onnx/releases/download/audio-tagging-models/sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 +// tar xvf sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 +// rm sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 +// +// clang-format on + +#include +#include +#include + +#include "sherpa-onnx/c-api/c-api.h" + +int32_t main() { + SherpaOnnxAudioTaggingConfig config; + memset(&config, 0, sizeof(config)); + + config.model.zipformer.model = + "./sherpa-onnx-zipformer-audio-tagging-2024-04-09/model.int8.onnx"; + config.model.num_threads = 1; + config.model.debug = 1; + config.model.provider = "cpu"; + // clang-format off + config.labels = "./sherpa-onnx-zipformer-audio-tagging-2024-04-09/class_labels_indices.csv"; + // clang-format on + + const SherpaOnnxAudioTagging *tagger = SherpaOnnxCreateAudioTagging(&config); + if (!tagger) { + fprintf(stderr, "Failed to create audio tagger. Please check your config"); + return -1; + } + + // You can find more test waves from + // https://github.com/k2-fsa/sherpa-onnx/releases/download/audio-tagging-models/sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 + const char *wav_filename = + "./sherpa-onnx-zipformer-audio-tagging-2024-04-09/test_wavs/1.wav"; + + const SherpaOnnxWave *wave = SherpaOnnxReadWave(wav_filename); + if (wave == NULL) { + fprintf(stderr, "Failed to read %s\n", wav_filename); + return -1; + } + + const SherpaOnnxOfflineStream *stream = + SherpaOnnxAudioTaggingCreateOfflineStream(tagger); + + AcceptWaveformOffline(stream, wave->sample_rate, wave->samples, + wave->num_samples); + + int32_t top_k = 5; + const SherpaOnnxAudioEvent *const *results = + SherpaOnnxAudioTaggingCompute(tagger, stream, top_k); + + fprintf(stderr, "--------------------------------------------------\n"); + fprintf(stderr, "Index\t\tProbability\t\tEvent name\n"); + fprintf(stderr, "--------------------------------------------------\n"); + for (int32_t i = 0; i != top_k; ++i) { + fprintf(stderr, "%d\t\t%.3f\t\t\t%s\n", i, results[i]->prob, + results[i]->name); + } + fprintf(stderr, "--------------------------------------------------\n"); + + SherpaOnnxAudioTaggingFreeResults(results); + DestroyOfflineStream(stream); + SherpaOnnxFreeWave(wave); + SherpaOnnxDestroyAudioTagging(tagger); + + return 0; +}; diff --git a/sherpa-onnx/c-api/c-api.cc b/sherpa-onnx/c-api/c-api.cc index c349dd3f2..995817a02 100644 --- a/sherpa-onnx/c-api/c-api.cc +++ b/sherpa-onnx/c-api/c-api.cc @@ -10,6 +10,7 @@ #include #include +#include "sherpa-onnx/csrc/audio-tagging.h" #include "sherpa-onnx/csrc/circular-buffer.h" #include "sherpa-onnx/csrc/display.h" #include "sherpa-onnx/csrc/keyword-spotter.h" @@ -400,15 +401,18 @@ SherpaOnnxOfflineStream *CreateOfflineStream( return stream; } -void DestroyOfflineStream(SherpaOnnxOfflineStream *stream) { delete stream; } +void DestroyOfflineStream(const SherpaOnnxOfflineStream *stream) { + delete stream; +} -void AcceptWaveformOffline(SherpaOnnxOfflineStream *stream, int32_t sample_rate, - const float *samples, int32_t n) { +void AcceptWaveformOffline(const SherpaOnnxOfflineStream *stream, + int32_t sample_rate, const float *samples, + int32_t n) { stream->impl->AcceptWaveform(sample_rate, samples, n); } -void DecodeOfflineStream(SherpaOnnxOfflineRecognizer *recognizer, - SherpaOnnxOfflineStream *stream) { +void DecodeOfflineStream(const SherpaOnnxOfflineRecognizer *recognizer, + const SherpaOnnxOfflineStream *stream) { recognizer->impl->DecodeStream(stream->impl.get()); } @@ -1209,3 +1213,89 @@ void SherpaOnnxSpeakerEmbeddingManagerFreeAllSpeakers( delete[] names; } + +struct SherpaOnnxAudioTagging { + std::unique_ptr impl; +}; + +const SherpaOnnxAudioTagging *SherpaOnnxCreateAudioTagging( + const SherpaOnnxAudioTaggingConfig *config) { + sherpa_onnx::AudioTaggingConfig ac; + ac.model.zipformer.model = SHERPA_ONNX_OR(config->model.zipformer.model, ""); + ac.model.num_threads = SHERPA_ONNX_OR(config->model.num_threads, 1); + ac.model.debug = config->model.debug; + ac.model.provider = SHERPA_ONNX_OR(config->model.provider, "cpu"); + ac.labels = SHERPA_ONNX_OR(config->labels, ""); + ac.top_k = SHERPA_ONNX_OR(config->top_k, 5); + + if (ac.model.debug) { + SHERPA_ONNX_LOGE("%s\n", ac.ToString().c_str()); + } + + if (!ac.Validate()) { + SHERPA_ONNX_LOGE("Errors in config"); + return nullptr; + } + + SherpaOnnxAudioTagging *tagger = new SherpaOnnxAudioTagging; + tagger->impl = std::make_unique(ac); + + return tagger; +} + +void SherpaOnnxDestroyAudioTagging(const SherpaOnnxAudioTagging *tagger) { + delete tagger; +} + +const SherpaOnnxOfflineStream *SherpaOnnxAudioTaggingCreateOfflineStream( + const SherpaOnnxAudioTagging *tagger) { + const SherpaOnnxOfflineStream *stream = + new SherpaOnnxOfflineStream(tagger->impl->CreateStream()); + return stream; +} + +const SherpaOnnxAudioEvent *const *SherpaOnnxAudioTaggingCompute( + const SherpaOnnxAudioTagging *tagger, const SherpaOnnxOfflineStream *s, + int32_t top_k) { + std::vector events = + tagger->impl->Compute(s->impl.get(), top_k); + + int32_t n = static_cast(events.size()); + SherpaOnnxAudioEvent **ans = new SherpaOnnxAudioEvent *[n + 1]; + ans[n] = nullptr; + + int32_t i = 0; + for (const auto &e : events) { + SherpaOnnxAudioEvent *p = new SherpaOnnxAudioEvent; + + char *name = new char[e.name.size() + 1]; + std::copy(e.name.begin(), e.name.end(), name); + name[e.name.size()] = 0; + + p->name = name; + + p->index = e.index; + p->prob = e.prob; + + ans[i] = p; + i += 1; + } + + return ans; +} + +void SherpaOnnxAudioTaggingFreeResults( + const SherpaOnnxAudioEvent *const *events) { + auto p = events; + + while (p && *p) { + auto e = *p; + + delete[] e->name; + delete e; + + ++p; + } + + delete[] events; +} diff --git a/sherpa-onnx/c-api/c-api.h b/sherpa-onnx/c-api/c-api.h index 276b35900..3833209ae 100644 --- a/sherpa-onnx/c-api/c-api.h +++ b/sherpa-onnx/c-api/c-api.h @@ -427,7 +427,8 @@ SHERPA_ONNX_API SherpaOnnxOfflineStream *CreateOfflineStream( /// Destroy an offline stream. /// /// @param stream A pointer returned by CreateOfflineStream() -SHERPA_ONNX_API void DestroyOfflineStream(SherpaOnnxOfflineStream *stream); +SHERPA_ONNX_API void DestroyOfflineStream( + const SherpaOnnxOfflineStream *stream); /// Accept input audio samples and compute the features. /// The user has to invoke DecodeOfflineStream() to run the neural network and @@ -442,9 +443,9 @@ SHERPA_ONNX_API void DestroyOfflineStream(SherpaOnnxOfflineStream *stream); /// @param n Number of elements in the samples array. /// /// @caution: For each offline stream, please invoke this function only once! -SHERPA_ONNX_API void AcceptWaveformOffline(SherpaOnnxOfflineStream *stream, - int32_t sample_rate, - const float *samples, int32_t n); +SHERPA_ONNX_API void AcceptWaveformOffline( + const SherpaOnnxOfflineStream *stream, int32_t sample_rate, + const float *samples, int32_t n); /// Decode an offline stream. /// /// We assume you have invoked AcceptWaveformOffline() for the given stream @@ -453,7 +454,8 @@ SHERPA_ONNX_API void AcceptWaveformOffline(SherpaOnnxOfflineStream *stream, /// @param recognizer A pointer returned by CreateOfflineRecognizer(). /// @param stream A pointer returned by CreateOfflineStream() SHERPA_ONNX_API void DecodeOfflineStream( - SherpaOnnxOfflineRecognizer *recognizer, SherpaOnnxOfflineStream *stream); + const SherpaOnnxOfflineRecognizer *recognizer, + const SherpaOnnxOfflineStream *stream); /// Decode a list offline streams in parallel. /// @@ -1088,6 +1090,65 @@ SherpaOnnxSpeakerEmbeddingManagerGetAllSpeakers( SHERPA_ONNX_API void SherpaOnnxSpeakerEmbeddingManagerFreeAllSpeakers( const char *const *names); +// ============================================================ +// For audio tagging +// ============================================================ +SHERPA_ONNX_API typedef struct + SherpaOnnxOfflineZipformerAudioTaggingModelConfig { + const char *model; +} SherpaOnnxOfflineZipformerAudioTaggingModelConfig; + +SHERPA_ONNX_API typedef struct SherpaOnnxAudioTaggingModelConfig { + SherpaOnnxOfflineZipformerAudioTaggingModelConfig zipformer; + int32_t num_threads; + int32_t debug; // true to print debug information of the model + const char *provider; +} SherpaOnnxAudioTaggingModelConfig; + +SHERPA_ONNX_API typedef struct SherpaOnnxAudioTaggingConfig { + SherpaOnnxAudioTaggingModelConfig model; + const char *labels; + int32_t top_k; +} SherpaOnnxAudioTaggingConfig; + +SHERPA_ONNX_API typedef struct SherpaOnnxAudioEvent { + const char *name; + int32_t index; + float prob; +} SherpaOnnxAudioEvent; + +SHERPA_ONNX_API typedef struct SherpaOnnxAudioTagging SherpaOnnxAudioTagging; + +// The user has to invoke +// SherpaOnnxDestroyAudioTagging() +// to free the returned pointer to avoid memory leak +SHERPA_ONNX_API const SherpaOnnxAudioTagging *SherpaOnnxCreateAudioTagging( + const SherpaOnnxAudioTaggingConfig *config); + +SHERPA_ONNX_API void SherpaOnnxDestroyAudioTagging( + const SherpaOnnxAudioTagging *tagger); + +// The user has to invoke DestroyOfflineStream() +// to free the returned pointer to avoid memory leak +SHERPA_ONNX_API const SherpaOnnxOfflineStream * +SherpaOnnxAudioTaggingCreateOfflineStream(const SherpaOnnxAudioTagging *tagger); + +// Return an array of pointers. The length of the array is top_k + 1. +// If top_k is -1, then config.top_k is used, where config is the config +// used to create the input tagger. +// +// The ans[0]->prob has the largest probability among the array elements +// The last element of the array is a null pointer +// +// The user has to use SherpaOnnxAudioTaggingFreeResults() +// to free the returned pointer to avoid memory leak +SHERPA_ONNX_API const SherpaOnnxAudioEvent *const * +SherpaOnnxAudioTaggingCompute(const SherpaOnnxAudioTagging *tagger, + const SherpaOnnxOfflineStream *s, int32_t top_k); + +SHERPA_ONNX_API void SherpaOnnxAudioTaggingFreeResults( + const SherpaOnnxAudioEvent *const *p); + #if defined(__GNUC__) #pragma GCC diagnostic pop #endif From 399d920b47fa168114471195772288f1e49d42a4 Mon Sep 17 00:00:00 2001 From: Manix <50542248+manickavela29@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:27:11 +0530 Subject: [PATCH 17/45] [feature] Configurable padding length in online websocket server (#755) Signed-off-by: manickavela29 --- sherpa-onnx/csrc/online-websocket-server-impl.cc | 8 ++++++-- sherpa-onnx/csrc/online-websocket-server-impl.h | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/sherpa-onnx/csrc/online-websocket-server-impl.cc b/sherpa-onnx/csrc/online-websocket-server-impl.cc index a47e613c8..ca9f2bf85 100644 --- a/sherpa-onnx/csrc/online-websocket-server-impl.cc +++ b/sherpa-onnx/csrc/online-websocket-server-impl.cc @@ -19,12 +19,17 @@ void OnlineWebsocketDecoderConfig::Register(ParseOptions *po) { po->Register("max-batch-size", &max_batch_size, "Max batch size for recognition."); + + po->Register("end-tail-padding", &end_tail_padding, + "It determines the length of tail_padding at the end of audio."); } void OnlineWebsocketDecoderConfig::Validate() const { recognizer_config.Validate(); SHERPA_ONNX_CHECK_GT(loop_interval_ms, 0); SHERPA_ONNX_CHECK_GT(max_batch_size, 0); + SHERPA_ONNX_CHECK_GT(end_tail_padding, 0); + } void OnlineWebsocketServerConfig::Register(sherpa_onnx::ParseOptions *po) { @@ -82,8 +87,7 @@ void OnlineWebsocketDecoder::InputFinished(std::shared_ptr c) { c->samples.pop_front(); } - // TODO(fangjun): Change the amount of paddings to be configurable - std::vector tail_padding(static_cast(0.8 * sample_rate)); + std::vector tail_padding(static_cast(config_.end_tail_padding * sample_rate)); c->s->AcceptWaveform(sample_rate, tail_padding.data(), tail_padding.size()); diff --git a/sherpa-onnx/csrc/online-websocket-server-impl.h b/sherpa-onnx/csrc/online-websocket-server-impl.h index a82170fba..9716c5c72 100644 --- a/sherpa-onnx/csrc/online-websocket-server-impl.h +++ b/sherpa-onnx/csrc/online-websocket-server-impl.h @@ -62,6 +62,8 @@ struct OnlineWebsocketDecoderConfig { int32_t max_batch_size = 5; + float end_tail_padding = 0.8; + void Register(ParseOptions *po); void Validate() const; }; From be4a2488a839fc57b861cf8141c93eea3a10d652 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Thu, 11 Apr 2024 15:58:11 +0800 Subject: [PATCH 18/45] Use batch size 1 in generating subtitles. (#756) --- python-api-examples/generate-subtitles.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python-api-examples/generate-subtitles.py b/python-api-examples/generate-subtitles.py index b1b9eca58..1f03d0d08 100755 --- a/python-api-examples/generate-subtitles.py +++ b/python-api-examples/generate-subtitles.py @@ -417,7 +417,9 @@ def main(): vad.pop() - recognizer.decode_streams(streams) + for s in streams: + recognizer.decode_stream(s) + for seg, stream in zip(segments, streams): seg.text = stream.result.text segment_list.append(seg) From 0f4705f775064da1711767e517eb49a5942d9506 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Fri, 12 Apr 2024 18:57:21 +0800 Subject: [PATCH 19/45] Fix WASM for kws (#758) --- wasm/kws/sherpa-onnx-kws.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wasm/kws/sherpa-onnx-kws.js b/wasm/kws/sherpa-onnx-kws.js index 936f2c908..22679dc5f 100644 --- a/wasm/kws/sherpa-onnx-kws.js +++ b/wasm/kws/sherpa-onnx-kws.js @@ -6,15 +6,15 @@ function freeConfig(config, Module) { } if ('transducer' in config) { - freeConfig(config.transducer); + freeConfig(config.transducer, Module); } if ('featConfig' in config) { - freeConfig(config.featConfig); + freeConfig(config.featConfig, Module); } if ('modelConfig' in config) { - freeConfig(config.modelConfig); + freeConfig(config.modelConfig, Module); } if ('keywordsBuffer' in config) { From 329fe1aa8b9c4d684e53def8d533552741f97681 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Sat, 13 Apr 2024 12:15:57 +0800 Subject: [PATCH 20/45] Support adding punctuations to the speech recogntion result (#761) --- .github/scripts/test-offline-punctuation.sh | 41 +++++ .github/workflows/linux.yaml | 10 ++ .github/workflows/macos.yaml | 10 ++ .github/workflows/windows-x64.yaml | 11 +- .github/workflows/windows-x86.yaml | 10 ++ cmake/cmake_extension.py | 3 +- ...entification-with-vad-non-streaming-asr.py | 3 + sherpa-onnx/csrc/CMakeLists.txt | 22 ++- sherpa-onnx/csrc/lexicon.cc | 5 - sherpa-onnx/csrc/macros.h | 18 ++ .../offline-ct-transformer-model-meta-data.h | 29 +++ .../csrc/offline-ct-transformer-model.cc | 164 +++++++++++++++++ .../csrc/offline-ct-transformer-model.h | 59 ++++++ .../offline-punctuation-ct-transformer-impl.h | 170 ++++++++++++++++++ sherpa-onnx/csrc/offline-punctuation-impl.cc | 22 +++ sherpa-onnx/csrc/offline-punctuation-impl.h | 27 +++ .../csrc/offline-punctuation-model-config.cc | 53 ++++++ .../csrc/offline-punctuation-model-config.h | 38 ++++ sherpa-onnx/csrc/offline-punctuation.cc | 42 +++++ sherpa-onnx/csrc/offline-punctuation.h | 47 +++++ .../csrc/online-websocket-server-impl.cc | 4 +- sherpa-onnx/csrc/session.cc | 5 + sherpa-onnx/csrc/session.h | 4 + .../csrc/sherpa-onnx-offline-punctuation.cc | 68 +++++++ sherpa-onnx/csrc/sherpa-onnx-offline.cc | 2 +- sherpa-onnx/csrc/text-utils.cc | 12 ++ sherpa-onnx/csrc/text-utils.h | 3 + 27 files changed, 866 insertions(+), 16 deletions(-) create mode 100755 .github/scripts/test-offline-punctuation.sh create mode 100644 sherpa-onnx/csrc/offline-ct-transformer-model-meta-data.h create mode 100644 sherpa-onnx/csrc/offline-ct-transformer-model.cc create mode 100644 sherpa-onnx/csrc/offline-ct-transformer-model.h create mode 100644 sherpa-onnx/csrc/offline-punctuation-ct-transformer-impl.h create mode 100644 sherpa-onnx/csrc/offline-punctuation-impl.cc create mode 100644 sherpa-onnx/csrc/offline-punctuation-impl.h create mode 100644 sherpa-onnx/csrc/offline-punctuation-model-config.cc create mode 100644 sherpa-onnx/csrc/offline-punctuation-model-config.h create mode 100644 sherpa-onnx/csrc/offline-punctuation.cc create mode 100644 sherpa-onnx/csrc/offline-punctuation.h create mode 100644 sherpa-onnx/csrc/sherpa-onnx-offline-punctuation.cc diff --git a/.github/scripts/test-offline-punctuation.sh b/.github/scripts/test-offline-punctuation.sh new file mode 100755 index 000000000..6a096c363 --- /dev/null +++ b/.github/scripts/test-offline-punctuation.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -ex + +log() { + # This function is from espnet + local fname=${BASH_SOURCE[1]##*/} + echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" +} + +echo "EXE is $EXE" +echo "PATH: $PATH" + +which $EXE + +log "------------------------------------------------------------" +log "Download model " +log "------------------------------------------------------------" + +curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/punctuation-models/sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +tar xvf sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +rm sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +repo=sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12 +ls -lh $repo + +$EXE \ + --debug=1 \ + --ct-transformer=$repo/model.onnx \ + "这是一个测试你好吗How are you我很好thank you are you ok谢谢你" + +$EXE \ + --debug=1 \ + --ct-transformer=$repo/model.onnx \ + "我们都是木头人不会说话不会动" + +$EXE \ + --debug=1 \ + --ct-transformer=$repo/model.onnx \ + "The African blogosphere is rapidly expanding bringing more voices online in the form of commentaries opinions analyses rants and poetry" + +rm -rf $repo diff --git a/.github/workflows/linux.yaml b/.github/workflows/linux.yaml index ee58cc408..0b8ba50f2 100644 --- a/.github/workflows/linux.yaml +++ b/.github/workflows/linux.yaml @@ -16,6 +16,7 @@ on: - '.github/scripts/test-online-ctc.sh' - '.github/scripts/test-offline-tts.sh' - '.github/scripts/test-audio-tagging.sh' + - '.github/scripts/test-offline-punctuation.sh' - 'CMakeLists.txt' - 'cmake/**' - 'sherpa-onnx/csrc/*' @@ -34,6 +35,7 @@ on: - '.github/scripts/test-online-ctc.sh' - '.github/scripts/test-offline-tts.sh' - '.github/scripts/test-audio-tagging.sh' + - '.github/scripts/test-offline-punctuation.sh' - 'CMakeLists.txt' - 'cmake/**' - 'sherpa-onnx/csrc/*' @@ -126,6 +128,14 @@ jobs: name: release-${{ matrix.build_type }}-with-shared-lib-${{ matrix.shared_lib }}-with-tts-${{ matrix.with_tts }} path: build/bin/* + - name: Test offline punctuation + shell: bash + run: | + export PATH=$PWD/build/bin:$PATH + export EXE=sherpa-onnx-offline-punctuation + + .github/scripts/test-offline-punctuation.sh + - name: Test C API shell: bash run: | diff --git a/.github/workflows/macos.yaml b/.github/workflows/macos.yaml index 99b4f301a..ecb2f8359 100644 --- a/.github/workflows/macos.yaml +++ b/.github/workflows/macos.yaml @@ -16,6 +16,7 @@ on: - '.github/scripts/test-offline-tts.sh' - '.github/scripts/test-online-ctc.sh' - '.github/scripts/test-audio-tagging.sh' + - '.github/scripts/test-offline-punctuation.sh' - 'CMakeLists.txt' - 'cmake/**' - 'sherpa-onnx/csrc/*' @@ -33,6 +34,7 @@ on: - '.github/scripts/test-offline-tts.sh' - '.github/scripts/test-online-ctc.sh' - '.github/scripts/test-audio-tagging.sh' + - '.github/scripts/test-offline-punctuation.sh' - 'CMakeLists.txt' - 'cmake/**' - 'sherpa-onnx/csrc/*' @@ -105,6 +107,14 @@ jobs: otool -L build/bin/sherpa-onnx otool -l build/bin/sherpa-onnx + - name: Test offline punctuation + shell: bash + run: | + export PATH=$PWD/build/bin:$PATH + export EXE=sherpa-onnx-offline-punctuation + + .github/scripts/test-offline-punctuation.sh + - name: Test C API shell: bash run: | diff --git a/.github/workflows/windows-x64.yaml b/.github/workflows/windows-x64.yaml index cf000be8f..55eedb374 100644 --- a/.github/workflows/windows-x64.yaml +++ b/.github/workflows/windows-x64.yaml @@ -15,6 +15,7 @@ on: - '.github/scripts/test-online-ctc.sh' - '.github/scripts/test-offline-tts.sh' - '.github/scripts/test-audio-tagging.sh' + - '.github/scripts/test-offline-punctuation.sh' - 'CMakeLists.txt' - 'cmake/**' - 'sherpa-onnx/csrc/*' @@ -30,6 +31,7 @@ on: - '.github/scripts/test-online-ctc.sh' - '.github/scripts/test-offline-tts.sh' - '.github/scripts/test-audio-tagging.sh' + - '.github/scripts/test-offline-punctuation.sh' - 'CMakeLists.txt' - 'cmake/**' - 'sherpa-onnx/csrc/*' @@ -72,6 +74,14 @@ jobs: ls -lh ./bin/Release/sherpa-onnx.exe + - name: Test offline punctuation + shell: bash + run: | + export PATH=$PWD/build/bin/Release:$PATH + export EXE=sherpa-onnx-offline-punctuation.exe + + .github/scripts/test-offline-punctuation.sh + - name: Test C API shell: bash run: | @@ -82,7 +92,6 @@ jobs: .github/scripts/test-c-api.sh - - name: Test Audio tagging shell: bash run: | diff --git a/.github/workflows/windows-x86.yaml b/.github/workflows/windows-x86.yaml index 7a18e0be1..b579487ae 100644 --- a/.github/workflows/windows-x86.yaml +++ b/.github/workflows/windows-x86.yaml @@ -15,6 +15,7 @@ on: - '.github/scripts/test-offline-tts.sh' - '.github/scripts/test-online-ctc.sh' - '.github/scripts/test-audio-tagging.sh' + - '.github/scripts/test-offline-punctuation.sh' - 'CMakeLists.txt' - 'cmake/**' - 'sherpa-onnx/csrc/*' @@ -30,6 +31,7 @@ on: - '.github/scripts/test-offline-tts.sh' - '.github/scripts/test-online-ctc.sh' - '.github/scripts/test-audio-tagging.sh' + - '.github/scripts/test-offline-punctuation.sh' - 'CMakeLists.txt' - 'cmake/**' - 'sherpa-onnx/csrc/*' @@ -72,6 +74,14 @@ jobs: ls -lh ./bin/Release/sherpa-onnx.exe + - name: Test offline punctuation + shell: bash + run: | + export PATH=$PWD/build/bin/Release:$PATH + export EXE=sherpa-onnx-offline-punctuation.exe + + .github/scripts/test-offline-punctuation.sh + - name: Test spoken language identification (C API) shell: bash run: | diff --git a/cmake/cmake_extension.py b/cmake/cmake_extension.py index ca57ff303..163276135 100644 --- a/cmake/cmake_extension.py +++ b/cmake/cmake_extension.py @@ -46,14 +46,15 @@ def enable_alsa(): def get_binaries(): binaries = [ "sherpa-onnx", - "sherpa-onnx-offline-audio-tagging", "sherpa-onnx-keyword-spotter", "sherpa-onnx-microphone", "sherpa-onnx-microphone-offline", "sherpa-onnx-microphone-offline-audio-tagging", "sherpa-onnx-microphone-offline-speaker-identification", "sherpa-onnx-offline", + "sherpa-onnx-offline-audio-tagging", "sherpa-onnx-offline-language-identification", + "sherpa-onnx-offline-punctuation", "sherpa-onnx-offline-tts", "sherpa-onnx-offline-tts-play", "sherpa-onnx-offline-websocket-server", diff --git a/python-api-examples/speaker-identification-with-vad-non-streaming-asr.py b/python-api-examples/speaker-identification-with-vad-non-streaming-asr.py index dfa54bb00..fe735e17a 100755 --- a/python-api-examples/speaker-identification-with-vad-non-streaming-asr.py +++ b/python-api-examples/speaker-identification-with-vad-non-streaming-asr.py @@ -408,8 +408,11 @@ def main(): vad_config.silero_vad.min_silence_duration = 0.25 vad_config.silero_vad.min_speech_duration = 0.25 vad_config.sample_rate = g_sample_rate + if not vad_config.validate(): + raise ValueError("Errors in vad config") window_size = vad_config.silero_vad.window_size + vad = sherpa_onnx.VoiceActivityDetector(vad_config, buffer_size_in_seconds=100) samples_per_read = int(0.1 * g_sample_rate) # 0.1 second = 100 ms diff --git a/sherpa-onnx/csrc/CMakeLists.txt b/sherpa-onnx/csrc/CMakeLists.txt index fe2a1a939..5ec490506 100644 --- a/sherpa-onnx/csrc/CMakeLists.txt +++ b/sherpa-onnx/csrc/CMakeLists.txt @@ -121,6 +121,14 @@ list(APPEND sources offline-zipformer-audio-tagging-model.cc ) +# punctuation +list(APPEND sources + offline-ct-transformer-model.cc + offline-punctuation-impl.cc + offline-punctuation-model-config.cc + offline-punctuation.cc +) + if(SHERPA_ONNX_ENABLE_TTS) list(APPEND sources lexicon.cc @@ -201,9 +209,10 @@ if(SHERPA_ONNX_ENABLE_BINARY) add_executable(sherpa-onnx sherpa-onnx.cc) add_executable(sherpa-onnx-keyword-spotter sherpa-onnx-keyword-spotter.cc) add_executable(sherpa-onnx-offline sherpa-onnx-offline.cc) - add_executable(sherpa-onnx-offline-parallel sherpa-onnx-offline-parallel.cc) - add_executable(sherpa-onnx-offline-language-identification sherpa-onnx-offline-language-identification.cc) add_executable(sherpa-onnx-offline-audio-tagging sherpa-onnx-offline-audio-tagging.cc) + add_executable(sherpa-onnx-offline-language-identification sherpa-onnx-offline-language-identification.cc) + add_executable(sherpa-onnx-offline-parallel sherpa-onnx-offline-parallel.cc) + add_executable(sherpa-onnx-offline-punctuation sherpa-onnx-offline-punctuation.cc) if(SHERPA_ONNX_ENABLE_TTS) add_executable(sherpa-onnx-offline-tts sherpa-onnx-offline-tts.cc) @@ -213,9 +222,10 @@ if(SHERPA_ONNX_ENABLE_BINARY) sherpa-onnx sherpa-onnx-keyword-spotter sherpa-onnx-offline - sherpa-onnx-offline-parallel - sherpa-onnx-offline-language-identification sherpa-onnx-offline-audio-tagging + sherpa-onnx-offline-language-identification + sherpa-onnx-offline-parallel + sherpa-onnx-offline-punctuation ) if(SHERPA_ONNX_ENABLE_TTS) list(APPEND main_exes @@ -260,11 +270,11 @@ endif() if(SHERPA_ONNX_HAS_ALSA AND SHERPA_ONNX_ENABLE_BINARY) add_executable(sherpa-onnx-alsa sherpa-onnx-alsa.cc alsa.cc) - add_executable(sherpa-onnx-keyword-spotter-alsa sherpa-onnx-keyword-spotter-alsa.cc alsa.cc) add_executable(sherpa-onnx-alsa-offline sherpa-onnx-alsa-offline.cc alsa.cc) + add_executable(sherpa-onnx-alsa-offline-audio-tagging sherpa-onnx-alsa-offline-audio-tagging.cc alsa.cc) add_executable(sherpa-onnx-alsa-offline-speaker-identification sherpa-onnx-alsa-offline-speaker-identification.cc alsa.cc) + add_executable(sherpa-onnx-keyword-spotter-alsa sherpa-onnx-keyword-spotter-alsa.cc alsa.cc) add_executable(sherpa-onnx-vad-alsa sherpa-onnx-vad-alsa.cc alsa.cc) - add_executable(sherpa-onnx-alsa-offline-audio-tagging sherpa-onnx-alsa-offline-audio-tagging.cc alsa.cc) if(SHERPA_ONNX_ENABLE_TTS) diff --git a/sherpa-onnx/csrc/lexicon.cc b/sherpa-onnx/csrc/lexicon.cc index e3a87eba3..2f176ddea 100644 --- a/sherpa-onnx/csrc/lexicon.cc +++ b/sherpa-onnx/csrc/lexicon.cc @@ -74,11 +74,6 @@ static std::vector ProcessHeteronyms( return ans; } -static void ToLowerCase(std::string *in_out) { - std::transform(in_out->begin(), in_out->end(), in_out->begin(), - [](unsigned char c) { return std::tolower(c); }); -} - // Note: We don't use SymbolTable here since tokens may contain a blank // in the first column static std::unordered_map ReadTokens(std::istream &is) { diff --git a/sherpa-onnx/csrc/macros.h b/sherpa-onnx/csrc/macros.h index 9ac3d302e..b5dfb99e3 100644 --- a/sherpa-onnx/csrc/macros.h +++ b/sherpa-onnx/csrc/macros.h @@ -118,6 +118,24 @@ } \ } while (0) +// read a vector of strings separated by sep +#define SHERPA_ONNX_READ_META_DATA_VEC_STRING_SEP(dst, src_key, sep) \ + do { \ + auto value = \ + meta_data.LookupCustomMetadataMapAllocated(src_key, allocator); \ + if (!value) { \ + SHERPA_ONNX_LOGE("%s does not exist in the metadata", src_key); \ + exit(-1); \ + } \ + SplitStringToVector(value.get(), sep, false, &dst); \ + \ + if (dst.empty()) { \ + SHERPA_ONNX_LOGE("Invalid value %s for %s. Empty vector!", value.get(), \ + src_key); \ + exit(-1); \ + } \ + } while (0) + // Read a string #define SHERPA_ONNX_READ_META_DATA_STR(dst, src_key) \ do { \ diff --git a/sherpa-onnx/csrc/offline-ct-transformer-model-meta-data.h b/sherpa-onnx/csrc/offline-ct-transformer-model-meta-data.h new file mode 100644 index 000000000..eea37d73e --- /dev/null +++ b/sherpa-onnx/csrc/offline-ct-transformer-model-meta-data.h @@ -0,0 +1,29 @@ +// sherpa-onnx/csrc/offline-ct-transformer-model-meta_data.h +// +// Copyright (c) 2024 Xiaomi Corporation +#ifndef SHERPA_ONNX_CSRC_OFFLINE_CT_TRANSFORMER_MODEL_META_DATA_H_ +#define SHERPA_ONNX_CSRC_OFFLINE_CT_TRANSFORMER_MODEL_META_DATA_H_ + +#include +#include +#include + +namespace sherpa_onnx { + +struct OfflineCtTransformerModelMetaData { + std::unordered_map token2id; + std::unordered_map punct2id; + std::vector id2punct; + + int32_t unk_id; + int32_t dot_id; + int32_t comma_id; + int32_t quest_id; + int32_t pause_id; + int32_t underline_id; + int32_t num_punctuations; +}; + +} // namespace sherpa_onnx + +#endif // SHERPA_ONNX_CSRC_OFFLINE_CT_TRANSFORMER_MODEL_META_DATA_H_ diff --git a/sherpa-onnx/csrc/offline-ct-transformer-model.cc b/sherpa-onnx/csrc/offline-ct-transformer-model.cc new file mode 100644 index 000000000..4452f7c76 --- /dev/null +++ b/sherpa-onnx/csrc/offline-ct-transformer-model.cc @@ -0,0 +1,164 @@ +// sherpa-onnx/csrc/offline-ct-transformer-model.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/csrc/offline-ct-transformer-model.h" + +#include +#include + +#include "sherpa-onnx/csrc/onnx-utils.h" +#include "sherpa-onnx/csrc/session.h" +#include "sherpa-onnx/csrc/text-utils.h" + +namespace sherpa_onnx { + +class OfflineCtTransformerModel::Impl { + public: + explicit Impl(const OfflinePunctuationModelConfig &config) + : config_(config), + env_(ORT_LOGGING_LEVEL_ERROR), + sess_opts_(GetSessionOptions(config)), + allocator_{} { + auto buf = ReadFile(config_.ct_transformer); + Init(buf.data(), buf.size()); + } + +#if __ANDROID_API__ >= 9 + Impl(AAssetManager *mgr, const OfflinePunctuationModelConfig &config) + : config_(config), + env_(ORT_LOGGING_LEVEL_ERROR), + sess_opts_(GetSessionOptions(config)), + allocator_{} { + auto buf = ReadFile(mgr, config_.ct_transformer); + Init(buf.data(), buf.size()); + } +#endif + + Ort::Value Forward(Ort::Value text, Ort::Value text_len) { + std::array inputs = {std::move(text), std::move(text_len)}; + + auto ans = + sess_->Run({}, input_names_ptr_.data(), inputs.data(), inputs.size(), + output_names_ptr_.data(), output_names_ptr_.size()); + return std::move(ans[0]); + } + + OrtAllocator *Allocator() const { return allocator_; } + + const OfflineCtTransformerModelMetaData &GetModelMetadata() const { + return meta_data_; + } + + private: + void Init(void *model_data, size_t model_data_length) { + sess_ = std::make_unique(env_, model_data, model_data_length, + sess_opts_); + + GetInputNames(sess_.get(), &input_names_, &input_names_ptr_); + + GetOutputNames(sess_.get(), &output_names_, &output_names_ptr_); + + // get meta data + Ort::ModelMetadata meta_data = sess_->GetModelMetadata(); + + Ort::AllocatorWithDefaultOptions allocator; // used in the macro below + + std::vector tokens; + SHERPA_ONNX_READ_META_DATA_VEC_STRING_SEP(tokens, "tokens", "|"); + + int32_t vocab_size; + SHERPA_ONNX_READ_META_DATA(vocab_size, "vocab_size"); + if (tokens.size() != vocab_size) { + SHERPA_ONNX_LOGE("tokens.size() %d != vocab_size %d", + static_cast(tokens.size()), vocab_size); + exit(-1); + } + + SHERPA_ONNX_READ_META_DATA_VEC_STRING_SEP(meta_data_.id2punct, + "punctuations", "|"); + + std::string unk_symbol; + SHERPA_ONNX_READ_META_DATA_STR(unk_symbol, "unk_symbol"); + + // output shape is (N, T, num_punctuations) + meta_data_.num_punctuations = + sess_->GetOutputTypeInfo(0).GetTensorTypeAndShapeInfo().GetShape()[2]; + + int32_t i = 0; + for (const auto &t : tokens) { + meta_data_.token2id[t] = i; + i += 1; + } + + i = 0; + for (const auto &p : meta_data_.id2punct) { + meta_data_.punct2id[p] = i; + i += 1; + } + + meta_data_.unk_id = meta_data_.token2id.at(unk_symbol); + + meta_data_.dot_id = meta_data_.punct2id.at("。"); + meta_data_.comma_id = meta_data_.punct2id.at(","); + meta_data_.quest_id = meta_data_.punct2id.at("?"); + meta_data_.pause_id = meta_data_.punct2id.at("、"); + meta_data_.underline_id = meta_data_.punct2id.at("_"); + + if (config_.debug) { + std::ostringstream os; + os << "vocab_size: " << meta_data_.token2id.size() << "\n"; + os << "num_punctuations: " << meta_data_.num_punctuations << "\n"; + os << "punctuations: "; + for (const auto &s : meta_data_.id2punct) { + os << s << " "; + } + os << "\n"; + SHERPA_ONNX_LOGE("\n%s\n", os.str().c_str()); + } + } + + private: + OfflinePunctuationModelConfig config_; + Ort::Env env_; + Ort::SessionOptions sess_opts_; + Ort::AllocatorWithDefaultOptions allocator_; + + std::unique_ptr sess_; + + std::vector input_names_; + std::vector input_names_ptr_; + + std::vector output_names_; + std::vector output_names_ptr_; + + OfflineCtTransformerModelMetaData meta_data_; +}; + +OfflineCtTransformerModel::OfflineCtTransformerModel( + const OfflinePunctuationModelConfig &config) + : impl_(std::make_unique(config)) {} + +#if __ANDROID_API__ >= 9 +OfflineCtTransformerModel::OfflineCtTransformerModel( + AAssetManager *mgr, const OfflinePunctuationModelConfig &config) + : impl_(std::make_unique(mgr, config)) {} +#endif + +OfflineCtTransformerModel::~OfflineCtTransformerModel() = default; + +Ort::Value OfflineCtTransformerModel::Forward(Ort::Value text, + Ort::Value text_len) const { + return impl_->Forward(std::move(text), std::move(text_len)); +} + +OrtAllocator *OfflineCtTransformerModel::Allocator() const { + return impl_->Allocator(); +} + +const OfflineCtTransformerModelMetaData & +OfflineCtTransformerModel::GetModelMetadata() const { + return impl_->GetModelMetadata(); +} + +} // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/offline-ct-transformer-model.h b/sherpa-onnx/csrc/offline-ct-transformer-model.h new file mode 100644 index 000000000..06e14ec7f --- /dev/null +++ b/sherpa-onnx/csrc/offline-ct-transformer-model.h @@ -0,0 +1,59 @@ +// sherpa-onnx/csrc/offline-ct-transformer-model.h +// +// Copyright (c) 2024 Xiaomi Corporation +#ifndef SHERPA_ONNX_CSRC_OFFLINE_CT_TRANSFORMER_MODEL_H_ +#define SHERPA_ONNX_CSRC_OFFLINE_CT_TRANSFORMER_MODEL_H_ +#include +#include + +#if __ANDROID_API__ >= 9 +#include "android/asset_manager.h" +#include "android/asset_manager_jni.h" +#endif + +#include "onnxruntime_cxx_api.h" // NOLINT +#include "sherpa-onnx/csrc/offline-ct-transformer-model-meta-data.h" +#include "sherpa-onnx/csrc/offline-punctuation-model-config.h" + +namespace sherpa_onnx { + +/** This class implements + * https://github.com/alibaba-damo-academy/FunASR/blob/main/runtime/python/onnxruntime/funasr_onnx/punc_bin.py#L17 + * from FunASR + */ +class OfflineCtTransformerModel { + public: + explicit OfflineCtTransformerModel( + const OfflinePunctuationModelConfig &config); + +#if __ANDROID_API__ >= 9 + OfflineCtTransformerModel(AAssetManager *mgr, + const OfflinePunctuationModelConfig &config); +#endif + + ~OfflineCtTransformerModel(); + + /** Run the forward method of the model. + * + * @param text A tensor of shape (N, T) of dtype int32. + * @param text A tensor of shape (N) of dtype int32. + * + * @return Return a tensor + * - punctuation_ids: A 2-D tensor of shape (N, T). + */ + Ort::Value Forward(Ort::Value text, Ort::Value text_len) const; + + /** Return an allocator for allocating memory + */ + OrtAllocator *Allocator() const; + + const OfflineCtTransformerModelMetaData &GetModelMetadata() const; + + private: + class Impl; + std::unique_ptr impl_; +}; + +} // namespace sherpa_onnx + +#endif // SHERPA_ONNX_CSRC_OFFLINE_CT_TRANSFORMER_MODEL_H_ diff --git a/sherpa-onnx/csrc/offline-punctuation-ct-transformer-impl.h b/sherpa-onnx/csrc/offline-punctuation-ct-transformer-impl.h new file mode 100644 index 000000000..134b8807c --- /dev/null +++ b/sherpa-onnx/csrc/offline-punctuation-ct-transformer-impl.h @@ -0,0 +1,170 @@ +// sherpa-onnx/csrc/offline-punctuation-ct-transformer-impl.h +// +// Copyright (c) 2024 Xiaomi Corporation +#ifndef SHERPA_ONNX_CSRC_OFFLINE_PUNCTUATION_CT_TRANSFORMER_IMPL_H_ +#define SHERPA_ONNX_CSRC_OFFLINE_PUNCTUATION_CT_TRANSFORMER_IMPL_H_ + +#include +#include +#include +#include + +#include "sherpa-onnx/csrc/macros.h" +#include "sherpa-onnx/csrc/math.h" +#include "sherpa-onnx/csrc/offline-ct-transformer-model.h" +#include "sherpa-onnx/csrc/offline-punctuation-impl.h" +#include "sherpa-onnx/csrc/offline-punctuation.h" +#include "sherpa-onnx/csrc/text-utils.h" + +namespace sherpa_onnx { + +class OfflinePunctuationCtTransformerImpl : public OfflinePunctuationImpl { + public: + explicit OfflinePunctuationCtTransformerImpl( + const OfflinePunctuationConfig &config) + : config_(config), model_(config.model) {} + + std::string AddPunctuation(const std::string &text) const override { + if (text.empty()) { + return {}; + } + + std::vector tokens = SplitUtf8(text); + std::vector token_ids; + token_ids.reserve(tokens.size()); + + const auto &meta_data = model_.GetModelMetadata(); + + for (const auto &t : tokens) { + std::string token = ToLowerCase(t); + if (meta_data.token2id.count(token)) { + token_ids.push_back(meta_data.token2id.at(token)); + } else { + token_ids.push_back(meta_data.unk_id); + } + } + + auto memory_info = + Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeDefault); + + int32_t segment_size = 20; + int32_t max_len = 200; + int32_t num_segments = (token_ids.size() + segment_size - 1) / segment_size; + + std::vector punctuations; + int32_t last = -1; + for (int32_t i = 0; i != num_segments; ++i) { + int32_t this_start = i * segment_size; // inclusive + int32_t this_end = this_start + segment_size; // exclusive + if (this_end > token_ids.size()) { + this_end = token_ids.size(); + } + + if (last != -1) { + this_start = last; + } + // token_ids[this_start:this_end] is sent to the model + + std::array x_shape = {1, this_end - this_start}; + Ort::Value x = + Ort::Value::CreateTensor(memory_info, token_ids.data() + this_start, + x_shape[1], x_shape.data(), x_shape.size()); + + int64_t len_shape = 1; + int32_t len = x_shape[1]; + Ort::Value x_len = + Ort::Value::CreateTensor(memory_info, &len, 1, &len_shape, 1); + + Ort::Value out = model_.Forward(std::move(x), std::move(x_len)); + + // [N, T, num_punctuations] + std::vector out_shape = + out.GetTensorTypeAndShapeInfo().GetShape(); + + assert(out_shape[0] == 1); + assert(out_shape[1] == len); + assert(out_shape[2] == meta_data.num_punctuations); + + std::vector this_punctuations; + this_punctuations.reserve(len); + + const float *p = out.GetTensorData(); + for (int32_t k = 0; k != len; ++k, p += meta_data.num_punctuations) { + auto index = static_cast(std::distance( + p, std::max_element(p, p + meta_data.num_punctuations))); + this_punctuations.push_back(index); + } // for (int32_t k = 0; k != len; ++k, p += meta_data.num_punctuations) + + int32_t dot_index = -1; + int32_t comma_index = -1; + + for (int32_t m = this_punctuations.size() - 1; m >= 1; --m) { + int32_t punct_id = this_punctuations[m]; + + if (punct_id == meta_data.dot_id || punct_id == meta_data.quest_id) { + dot_index = m; + break; + } + + if (comma_index == -1 && punct_id == meta_data.comma_id) { + comma_index = m; + } + } // for (int32_t k = this_punctuations.size() - 1; k >= 1; --k) + + if (dot_index == -1 && len >= max_len && comma_index != -1) { + dot_index = comma_index; + this_punctuations[dot_index] = meta_data.dot_id; + } + + if (dot_index == -1) { + if (last == -1) { + last = this_start; + } + + if (i == num_segments - 1) { + dot_index = token_ids.size() - 1; + } + } else { + last = this_start + dot_index + 1; + + punctuations.insert(punctuations.end(), this_punctuations.begin(), + this_punctuations.begin() + (dot_index + 1)); + } + } // for (int32_t i = 0; i != num_segments; ++i) + + if (punctuations.size() != token_ids.size() && + punctuations.size() + 1 == token_ids.size()) { + punctuations.push_back(meta_data.dot_id); + } + + if (punctuations.size() != token_ids.size()) { + SHERPA_ONNX_LOGE("%s, %d, %d. Some unexpected things happened", + text.c_str(), static_cast(punctuations.size()), + static_cast(token_ids.size())); + return text; + } + + std::string ans; + + for (int32_t i = 0; i != static_cast(punctuations.size()); ++i) { + const std::string &w = tokens[i]; + if (i > 0 && !(ans.back() & 0x80) && !(w[0] & 0x80)) { + ans.push_back(' '); + } + ans.append(w); + if (punctuations[i] != meta_data.underline_id) { + ans.append(meta_data.id2punct[punctuations[i]]); + } + } + + return ans; + } + + private: + OfflinePunctuationConfig config_; + OfflineCtTransformerModel model_; +}; + +} // namespace sherpa_onnx + +#endif // SHERPA_ONNX_CSRC_OFFLINE_PUNCTUATION_CT_TRANSFORMER_IMPL_H_ diff --git a/sherpa-onnx/csrc/offline-punctuation-impl.cc b/sherpa-onnx/csrc/offline-punctuation-impl.cc new file mode 100644 index 000000000..2eefdae39 --- /dev/null +++ b/sherpa-onnx/csrc/offline-punctuation-impl.cc @@ -0,0 +1,22 @@ +// sherpa-onnx/csrc/offline-punctuation-impl.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/csrc/offline-punctuation-impl.h" + +#include "sherpa-onnx/csrc/macros.h" +#include "sherpa-onnx/csrc/offline-punctuation-ct-transformer-impl.h" + +namespace sherpa_onnx { + +std::unique_ptr OfflinePunctuationImpl::Create( + const OfflinePunctuationConfig &config) { + if (!config.model.ct_transformer.empty()) { + return std::make_unique(config); + } + + SHERPA_ONNX_LOGE("Please specify a punctuation model! Return a null pointer"); + return nullptr; +} + +} // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/offline-punctuation-impl.h b/sherpa-onnx/csrc/offline-punctuation-impl.h new file mode 100644 index 000000000..7e1c1c1bc --- /dev/null +++ b/sherpa-onnx/csrc/offline-punctuation-impl.h @@ -0,0 +1,27 @@ +// sherpa-onnx/csrc/offline-punctuation-impl.h +// +// Copyright (c) 2024 Xiaomi Corporation +#ifndef SHERPA_ONNX_CSRC_OFFLINE_PUNCTUATION_IMPL_H_ +#define SHERPA_ONNX_CSRC_OFFLINE_PUNCTUATION_IMPL_H_ + +#include +#include +#include + +#include "sherpa-onnx/csrc/offline-punctuation.h" + +namespace sherpa_onnx { + +class OfflinePunctuationImpl { + public: + virtual ~OfflinePunctuationImpl() = default; + + static std::unique_ptr Create( + const OfflinePunctuationConfig &config); + + virtual std::string AddPunctuation(const std::string &text) const = 0; +}; + +} // namespace sherpa_onnx + +#endif // SHERPA_ONNX_CSRC_OFFLINE_PUNCTUATION_IMPL_H_ diff --git a/sherpa-onnx/csrc/offline-punctuation-model-config.cc b/sherpa-onnx/csrc/offline-punctuation-model-config.cc new file mode 100644 index 000000000..e98fe00b2 --- /dev/null +++ b/sherpa-onnx/csrc/offline-punctuation-model-config.cc @@ -0,0 +1,53 @@ +// sherpa-onnx/csrc/offline-punctuation-model-config.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/csrc/offline-punctuation-model-config.h" + +#include "sherpa-onnx/csrc/file-utils.h" +#include "sherpa-onnx/csrc/macros.h" + +namespace sherpa_onnx { + +void OfflinePunctuationModelConfig::Register(ParseOptions *po) { + po->Register("ct-transformer", &ct_transformer, + "Path to the controllable time-delay (CT) transformer model"); + + po->Register("num-threads", &num_threads, + "Number of threads to run the neural network"); + + po->Register("debug", &debug, + "true to print model information while loading it."); + + po->Register("provider", &provider, + "Specify a provider to use: cpu, cuda, coreml"); +} + +bool OfflinePunctuationModelConfig::Validate() const { + if (ct_transformer.empty()) { + SHERPA_ONNX_LOGE("Please provide --ct-transformer"); + return false; + } + + if (!FileExists(ct_transformer)) { + SHERPA_ONNX_LOGE("--ct-transformer %s does not exist", + ct_transformer.c_str()); + return false; + } + + return true; +} + +std::string OfflinePunctuationModelConfig::ToString() const { + std::ostringstream os; + + os << "OfflinePunctuationModelConfig("; + os << "ct_transformer=\"" << ct_transformer << "\", "; + os << "num_threads=" << num_threads << ", "; + os << "debug=" << (debug ? "True" : "False") << ", "; + os << "provider=\"" << provider << "\")"; + + return os.str(); +} + +} // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/offline-punctuation-model-config.h b/sherpa-onnx/csrc/offline-punctuation-model-config.h new file mode 100644 index 000000000..aa294f3ff --- /dev/null +++ b/sherpa-onnx/csrc/offline-punctuation-model-config.h @@ -0,0 +1,38 @@ +// sherpa-onnx/csrc/offline-punctuation-model-config.h +// +// Copyright (c) 2024 Xiaomi Corporation +#ifndef SHERPA_ONNX_CSRC_OFFLINE_PUNCTUATION_MODEL_CONFIG_H_ +#define SHERPA_ONNX_CSRC_OFFLINE_PUNCTUATION_MODEL_CONFIG_H_ + +#include + +#include "sherpa-onnx/csrc/parse-options.h" + +namespace sherpa_onnx { + +struct OfflinePunctuationModelConfig { + std::string ct_transformer; + + int32_t num_threads = 1; + bool debug = false; + std::string provider = "cpu"; + + OfflinePunctuationModelConfig() = default; + + OfflinePunctuationModelConfig(const std::string &ct_transformer, + int32_t num_threads, bool debug, + const std::string &provider) + : ct_transformer(ct_transformer), + num_threads(num_threads), + debug(debug), + provider(provider) {} + + void Register(ParseOptions *po); + bool Validate() const; + + std::string ToString() const; +}; + +} // namespace sherpa_onnx + +#endif // SHERPA_ONNX_CSRC_OFFLINE_PUNCTUATION_MODEL_CONFIG_H_ diff --git a/sherpa-onnx/csrc/offline-punctuation.cc b/sherpa-onnx/csrc/offline-punctuation.cc new file mode 100644 index 000000000..292156ab8 --- /dev/null +++ b/sherpa-onnx/csrc/offline-punctuation.cc @@ -0,0 +1,42 @@ +// sherpa-onnx/csrc/offline-punctuation.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/csrc/offline-punctuation.h" + +#include "sherpa-onnx/csrc/macros.h" +#include "sherpa-onnx/csrc/offline-punctuation-impl.h" + +namespace sherpa_onnx { + +void OfflinePunctuationConfig::Register(ParseOptions *po) { + model.Register(po); +} + +bool OfflinePunctuationConfig::Validate() const { + if (!model.Validate()) { + return false; + } + + return true; +} + +std::string OfflinePunctuationConfig::ToString() const { + std::ostringstream os; + + os << "OfflinePunctuationConfig("; + os << "model=" << model.ToString() << ")"; + + return os.str(); +} + +OfflinePunctuation::OfflinePunctuation(const OfflinePunctuationConfig &config) + : impl_(OfflinePunctuationImpl::Create(config)) {} + +OfflinePunctuation::~OfflinePunctuation() = default; + +std::string OfflinePunctuation::AddPunctuation(const std::string &text) const { + return impl_->AddPunctuation(text); +} + +} // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/offline-punctuation.h b/sherpa-onnx/csrc/offline-punctuation.h new file mode 100644 index 000000000..7be31e413 --- /dev/null +++ b/sherpa-onnx/csrc/offline-punctuation.h @@ -0,0 +1,47 @@ +// sherpa-onnx/csrc/offline-punctuation.h +// +// Copyright (c) 2024 Xiaomi Corporation +#ifndef SHERPA_ONNX_CSRC_OFFLINE_PUNCTUATION_H_ +#define SHERPA_ONNX_CSRC_OFFLINE_PUNCTUATION_H_ + +#include +#include +#include + +#include "sherpa-onnx/csrc/offline-punctuation-model-config.h" +#include "sherpa-onnx/csrc/parse-options.h" + +namespace sherpa_onnx { + +struct OfflinePunctuationConfig { + OfflinePunctuationModelConfig model; + + OfflinePunctuationConfig() = default; + + explicit OfflinePunctuationConfig(const OfflinePunctuationModelConfig &model) + : model(model) {} + + void Register(ParseOptions *po); + bool Validate() const; + + std::string ToString() const; +}; + +class OfflinePunctuationImpl; + +class OfflinePunctuation { + public: + explicit OfflinePunctuation(const OfflinePunctuationConfig &config); + + ~OfflinePunctuation(); + + // Add punctuation to the input text and return it. + std::string AddPunctuation(const std::string &text) const; + + private: + std::unique_ptr impl_; +}; + +} // namespace sherpa_onnx + +#endif // SHERPA_ONNX_CSRC_OFFLINE_PUNCTUATION_H_ diff --git a/sherpa-onnx/csrc/online-websocket-server-impl.cc b/sherpa-onnx/csrc/online-websocket-server-impl.cc index ca9f2bf85..d02a4913e 100644 --- a/sherpa-onnx/csrc/online-websocket-server-impl.cc +++ b/sherpa-onnx/csrc/online-websocket-server-impl.cc @@ -29,7 +29,6 @@ void OnlineWebsocketDecoderConfig::Validate() const { SHERPA_ONNX_CHECK_GT(loop_interval_ms, 0); SHERPA_ONNX_CHECK_GT(max_batch_size, 0); SHERPA_ONNX_CHECK_GT(end_tail_padding, 0); - } void OnlineWebsocketServerConfig::Register(sherpa_onnx::ParseOptions *po) { @@ -87,7 +86,8 @@ void OnlineWebsocketDecoder::InputFinished(std::shared_ptr c) { c->samples.pop_front(); } - std::vector tail_padding(static_cast(config_.end_tail_padding * sample_rate)); + std::vector tail_padding( + static_cast(config_.end_tail_padding * sample_rate)); c->s->AcceptWaveform(sample_rate, tail_padding.data(), tail_padding.size()); diff --git a/sherpa-onnx/csrc/session.cc b/sherpa-onnx/csrc/session.cc index d555ed7a7..d0a697404 100644 --- a/sherpa-onnx/csrc/session.cc +++ b/sherpa-onnx/csrc/session.cc @@ -160,4 +160,9 @@ Ort::SessionOptions GetSessionOptions(const AudioTaggingModelConfig &config) { return GetSessionOptionsImpl(config.num_threads, config.provider); } +Ort::SessionOptions GetSessionOptions( + const OfflinePunctuationModelConfig &config) { + return GetSessionOptionsImpl(config.num_threads, config.provider); +} + } // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/session.h b/sherpa-onnx/csrc/session.h index 94f263fd9..a4121436a 100644 --- a/sherpa-onnx/csrc/session.h +++ b/sherpa-onnx/csrc/session.h @@ -9,6 +9,7 @@ #include "sherpa-onnx/csrc/audio-tagging-model-config.h" #include "sherpa-onnx/csrc/offline-lm-config.h" #include "sherpa-onnx/csrc/offline-model-config.h" +#include "sherpa-onnx/csrc/offline-punctuation-model-config.h" #include "sherpa-onnx/csrc/online-lm-config.h" #include "sherpa-onnx/csrc/online-model-config.h" #include "sherpa-onnx/csrc/speaker-embedding-extractor.h" @@ -43,6 +44,9 @@ Ort::SessionOptions GetSessionOptions( Ort::SessionOptions GetSessionOptions(const AudioTaggingModelConfig &config); +Ort::SessionOptions GetSessionOptions( + const OfflinePunctuationModelConfig &config); + } // namespace sherpa_onnx #endif // SHERPA_ONNX_CSRC_SESSION_H_ diff --git a/sherpa-onnx/csrc/sherpa-onnx-offline-punctuation.cc b/sherpa-onnx/csrc/sherpa-onnx-offline-punctuation.cc new file mode 100644 index 000000000..7f2207344 --- /dev/null +++ b/sherpa-onnx/csrc/sherpa-onnx-offline-punctuation.cc @@ -0,0 +1,68 @@ +// sherpa-onnx/csrc/sherpa-onnx-offline-punctuation.cc +// +// Copyright (c) 2022-2024 Xiaomi Corporation +#include + +#include // NOLINT + +#include "sherpa-onnx/csrc/offline-punctuation.h" +#include "sherpa-onnx/csrc/parse-options.h" + +int main(int32_t argc, char *argv[]) { + const char *kUsageMessage = R"usage( +Add punctuations to the input text. + +The input text can contain both Chinese and English words. + +Usage: + +wget https://github.com/k2-fsa/sherpa-onnx/releases/download/punctuation-models/sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +tar xvf sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +rm sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 + +./bin/sherpa-onnx-offline-punctuation \ + --ct-transformer=./sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12/model.onnx + "你好吗how are you Fantasitic 谢谢我很好你怎么样呢" + +The output text should look like below: +)usage"; + + sherpa_onnx::ParseOptions po(kUsageMessage); + sherpa_onnx::OfflinePunctuationConfig config; + config.Register(&po); + po.Read(argc, argv); + if (po.NumArgs() != 1) { + fprintf(stderr, + "Error: Please provide only 1 position argument containing the " + "input text.\n\n"); + po.PrintUsage(); + exit(EXIT_FAILURE); + } + + fprintf(stderr, "%s\n", config.ToString().c_str()); + + if (!config.Validate()) { + fprintf(stderr, "Errors in config!\n"); + return -1; + } + + fprintf(stderr, "Creating OfflinePunctuation ...\n"); + sherpa_onnx::OfflinePunctuation punct(config); + fprintf(stderr, "Started\n"); + const auto begin = std::chrono::steady_clock::now(); + + std::string text = po.GetArg(1); + std::string text_with_punct = punct.AddPunctuation(text); + fprintf(stderr, "Done\n"); + const auto end = std::chrono::steady_clock::now(); + + float elapsed_seconds = + std::chrono::duration_cast(end - begin) + .count() / + 1000.; + + fprintf(stderr, "Num threads: %d\n", config.model.num_threads); + fprintf(stderr, "Elapsed seconds: %.3f s\n", elapsed_seconds); + fprintf(stderr, "Input text: %s\n", text.c_str()); + fprintf(stderr, "Output text: %s\n", text_with_punct.c_str()); +} diff --git a/sherpa-onnx/csrc/sherpa-onnx-offline.cc b/sherpa-onnx/csrc/sherpa-onnx-offline.cc index e8c1a7b29..a84266c72 100644 --- a/sherpa-onnx/csrc/sherpa-onnx-offline.cc +++ b/sherpa-onnx/csrc/sherpa-onnx-offline.cc @@ -111,8 +111,8 @@ for a list of pre-trained models to download. fprintf(stderr, "Creating recognizer ...\n"); sherpa_onnx::OfflineRecognizer recognizer(config); - const auto begin = std::chrono::steady_clock::now(); fprintf(stderr, "Started\n"); + const auto begin = std::chrono::steady_clock::now(); std::vector> ss; std::vector ss_pointers; diff --git a/sherpa-onnx/csrc/text-utils.cc b/sherpa-onnx/csrc/text-utils.cc index c01c31b3f..04586dd8c 100644 --- a/sherpa-onnx/csrc/text-utils.cc +++ b/sherpa-onnx/csrc/text-utils.cc @@ -385,4 +385,16 @@ std::vector SplitUtf8(const std::string &text) { return MergeCharactersIntoWords(ans); } +std::string ToLowerCase(const std::string &s) { + std::string ans(s.size(), 0); + std::transform(s.begin(), s.end(), ans.begin(), + [](unsigned char c) { return std::tolower(c); }); + return ans; +} + +void ToLowerCase(std::string *in_out) { + std::transform(in_out->begin(), in_out->end(), in_out->begin(), + [](unsigned char c) { return std::tolower(c); }); +} + } // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/text-utils.h b/sherpa-onnx/csrc/text-utils.h index 07251eef9..a0b968d8a 100644 --- a/sherpa-onnx/csrc/text-utils.h +++ b/sherpa-onnx/csrc/text-utils.h @@ -121,6 +121,9 @@ bool ConvertStringToReal(const std::string &str, T *out); std::vector SplitUtf8(const std::string &text); +std::string ToLowerCase(const std::string &s); +void ToLowerCase(std::string *in_out); + } // namespace sherpa_onnx #endif // SHERPA_ONNX_CSRC_TEXT_UTILS_H_ From 68b8b88b5a01a8064f9fa04d3ecb67e675a01b61 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Sat, 13 Apr 2024 13:28:17 +0800 Subject: [PATCH 21/45] Add Python API for punctuation models. (#762) --- .github/scripts/test-offline-punctuation.sh | 2 +- .github/scripts/test-python.sh | 12 +++++ .gitignore | 1 + go-api-examples/vad-asr-paraformer/run.sh | 2 +- go-api-examples/vad-asr-whisper/run.sh | 2 +- .../vad-speaker-identification/run.sh | 2 +- .../vad-spoken-language-identification/run.sh | 2 +- go-api-examples/vad/run.sh | 2 +- python-api-examples/add-punctuation.py | 46 +++++++++++++++++ sherpa-onnx/python/csrc/CMakeLists.txt | 1 + .../python/csrc/offline-punctuation.cc | 49 +++++++++++++++++++ sherpa-onnx/python/csrc/offline-punctuation.h | 16 ++++++ sherpa-onnx/python/csrc/sherpa-onnx.cc | 2 + sherpa-onnx/python/sherpa_onnx/__init__.py | 3 ++ 14 files changed, 136 insertions(+), 6 deletions(-) create mode 100755 python-api-examples/add-punctuation.py create mode 100644 sherpa-onnx/python/csrc/offline-punctuation.cc create mode 100644 sherpa-onnx/python/csrc/offline-punctuation.h diff --git a/.github/scripts/test-offline-punctuation.sh b/.github/scripts/test-offline-punctuation.sh index 6a096c363..bca0ede08 100755 --- a/.github/scripts/test-offline-punctuation.sh +++ b/.github/scripts/test-offline-punctuation.sh @@ -14,7 +14,7 @@ echo "PATH: $PATH" which $EXE log "------------------------------------------------------------" -log "Download model " +log "Download the punctuation model " log "------------------------------------------------------------" curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/punctuation-models/sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 diff --git a/.github/scripts/test-python.sh b/.github/scripts/test-python.sh index aa9b795f1..fe0f568f0 100755 --- a/.github/scripts/test-python.sh +++ b/.github/scripts/test-python.sh @@ -8,6 +8,18 @@ log() { echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" } +log "test offline punctuation" + +curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/punctuation-models/sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +tar xvf sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +rm sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +repo=sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12 +ls -lh $repo + +python3 ./python-api-examples/add-punctuation.py + +rm -rf $repo + log "test audio tagging" curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/audio-tagging-models/sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 diff --git a/.gitignore b/.gitignore index a51cd0ea3..3047a1e0a 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,4 @@ sr-data *xcworkspace/xcuserdata/* vits-icefall-* +sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12 diff --git a/go-api-examples/vad-asr-paraformer/run.sh b/go-api-examples/vad-asr-paraformer/run.sh index c2f65d9b0..9d136804e 100755 --- a/go-api-examples/vad-asr-paraformer/run.sh +++ b/go-api-examples/vad-asr-paraformer/run.sh @@ -2,7 +2,7 @@ if [ ! -f ./silero_vad.onnx ]; then - curl -SL -O https://github.com/snakers4/silero-vad/blob/master/files/silero_vad.onnx + curl -SL -O https://github.com/snakers4/silero-vad/raw/master/files/silero_vad.onnx fi if [ ! -f ./sherpa-onnx-paraformer-trilingual-zh-cantonese-en/model.int8.onnx ]; then diff --git a/go-api-examples/vad-asr-whisper/run.sh b/go-api-examples/vad-asr-whisper/run.sh index 2ae3b5af8..8064887d5 100755 --- a/go-api-examples/vad-asr-whisper/run.sh +++ b/go-api-examples/vad-asr-whisper/run.sh @@ -2,7 +2,7 @@ if [ ! -f ./silero_vad.onnx ]; then - curl -SL -O https://github.com/snakers4/silero-vad/blob/master/files/silero_vad.onnx + curl -SL -O https://github.com/snakers4/silero-vad/raw/master/files/silero_vad.onnx fi if [ ! -f ./sherpa-onnx-whisper-tiny.en/tiny.en-encoder.int8.onnx ]; then diff --git a/go-api-examples/vad-speaker-identification/run.sh b/go-api-examples/vad-speaker-identification/run.sh index e500f5c00..1df026786 100755 --- a/go-api-examples/vad-speaker-identification/run.sh +++ b/go-api-examples/vad-speaker-identification/run.sh @@ -9,7 +9,7 @@ if [ ! -f ./sr-data/enroll/fangjun-sr-1.wav ]; then fi if [ ! -f ./silero_vad.onnx ]; then - curl -SL -O https://github.com/snakers4/silero-vad/blob/master/files/silero_vad.onnx + curl -SL -O https://github.com/snakers4/silero-vad/raw/master/files/silero_vad.onnx fi go mod tidy diff --git a/go-api-examples/vad-spoken-language-identification/run.sh b/go-api-examples/vad-spoken-language-identification/run.sh index fc3c219e7..43ae2525b 100755 --- a/go-api-examples/vad-spoken-language-identification/run.sh +++ b/go-api-examples/vad-spoken-language-identification/run.sh @@ -2,7 +2,7 @@ if [ ! -f ./silero_vad.onnx ]; then - curl -SL -O https://github.com/snakers4/silero-vad/blob/master/files/silero_vad.onnx + curl -SL -O https://github.com/snakers4/silero-vad/raw/master/files/silero_vad.onnx fi if [ ! -f ./sherpa-onnx-whisper-tiny/tiny-encoder.int8.onnx ]; then diff --git a/go-api-examples/vad/run.sh b/go-api-examples/vad/run.sh index 1584b99fe..13e505ce6 100755 --- a/go-api-examples/vad/run.sh +++ b/go-api-examples/vad/run.sh @@ -2,7 +2,7 @@ if [ ! -f ./silero_vad.onnx ]; then - curl -SL -O https://github.com/snakers4/silero-vad/blob/master/files/silero_vad.onnx + curl -SL -O https://github.com/snakers4/silero-vad/raw/master/files/silero_vad.onnx fi go mod tidy diff --git a/python-api-examples/add-punctuation.py b/python-api-examples/add-punctuation.py new file mode 100755 index 000000000..7db3e1904 --- /dev/null +++ b/python-api-examples/add-punctuation.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +""" +This script shows how to add punctuations to text using sherpa-onnx Python API. + +Please download the model from +https://github.com/k2-fsa/sherpa-onnx/releases/tag/punctuation-models + +The following is an example + +wget https://github.com/k2-fsa/sherpa-onnx/releases/download/punctuation-models/sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +tar xvf sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +rm sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +""" + +from pathlib import Path + +import sherpa_onnx + + +def main(): + model = "./sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12/model.onnx" + if not Path(model).is_file(): + raise ValueError(f"{model} does not exist") + config = sherpa_onnx.OfflinePunctuationConfig( + model=sherpa_onnx.OfflinePunctuationModelConfig(ct_transformer=model), + ) + + punct = sherpa_onnx.OfflinePunctuation(config) + + text_list = [ + "这是一个测试你好吗How are you我很好thank you are you ok谢谢你", + "我们都是木头人不会说话不会动", + "The African blogosphere is rapidly expanding bringing more voices online in the form of commentaries opinions analyses rants and poetry", + ] + for text in text_list: + text_with_punct = punct.add_punctuation(text) + print("----------") + print(f"input: {text}") + print(f"output: {text_with_punct}") + + print("----------") + + +if __name__ == "__main__": + main() diff --git a/sherpa-onnx/python/csrc/CMakeLists.txt b/sherpa-onnx/python/csrc/CMakeLists.txt index 266b7c312..e4bff01c6 100644 --- a/sherpa-onnx/python/csrc/CMakeLists.txt +++ b/sherpa-onnx/python/csrc/CMakeLists.txt @@ -12,6 +12,7 @@ set(srcs offline-model-config.cc offline-nemo-enc-dec-ctc-model-config.cc offline-paraformer-model-config.cc + offline-punctuation.cc offline-recognizer.cc offline-stream.cc offline-tdnn-model-config.cc diff --git a/sherpa-onnx/python/csrc/offline-punctuation.cc b/sherpa-onnx/python/csrc/offline-punctuation.cc new file mode 100644 index 000000000..7d3ff86d8 --- /dev/null +++ b/sherpa-onnx/python/csrc/offline-punctuation.cc @@ -0,0 +1,49 @@ +// sherpa-onnx/python/csrc/offline-punctuation.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/python/csrc/offline-punctuation.h" + +#include "sherpa-onnx/csrc/offline-punctuation.h" + +namespace sherpa_onnx { + +static void PybindOfflinePunctuationModelConfig(py::module *m) { + using PyClass = OfflinePunctuationModelConfig; + py::class_(*m, "OfflinePunctuationModelConfig") + .def(py::init<>()) + .def(py::init(), + py::arg("ct_transformer"), py::arg("num_threads") = 1, + py::arg("debug") = false, py::arg("provider") = "cpu") + .def_readwrite("ct_transformer", &PyClass::ct_transformer) + .def_readwrite("num_threads", &PyClass::num_threads) + .def_readwrite("debug", &PyClass::debug) + .def_readwrite("provider", &PyClass::provider) + .def("validate", &PyClass::Validate) + .def("__str__", &PyClass::ToString); +} + +static void PybindOfflinePunctuationConfig(py::module *m) { + PybindOfflinePunctuationModelConfig(m); + using PyClass = OfflinePunctuationConfig; + + py::class_(*m, "OfflinePunctuationConfig") + .def(py::init<>()) + .def(py::init(), py::arg("model")) + .def_readwrite("model", &PyClass::model) + .def("validate", &PyClass::Validate) + .def("__str__", &PyClass::ToString); +} + +void PybindOfflinePunctuation(py::module *m) { + PybindOfflinePunctuationConfig(m); + using PyClass = OfflinePunctuation; + + py::class_(*m, "OfflinePunctuation") + .def(py::init(), py::arg("config"), + py::call_guard()) + .def("add_punctuation", &PyClass::AddPunctuation, py::arg("text"), + py::call_guard()); +} + +} // namespace sherpa_onnx diff --git a/sherpa-onnx/python/csrc/offline-punctuation.h b/sherpa-onnx/python/csrc/offline-punctuation.h new file mode 100644 index 000000000..015f94769 --- /dev/null +++ b/sherpa-onnx/python/csrc/offline-punctuation.h @@ -0,0 +1,16 @@ +// sherpa-onnx/python/csrc/offline-punctuation.h +// +// Copyright (c) 2024 Xiaomi Corporation + +#ifndef SHERPA_ONNX_PYTHON_CSRC_OFFLINE_PUNCTUATION_H_ +#define SHERPA_ONNX_PYTHON_CSRC_OFFLINE_PUNCTUATION_H_ + +#include "sherpa-onnx/python/csrc/sherpa-onnx.h" + +namespace sherpa_onnx { + +void PybindOfflinePunctuation(py::module *m); + +} + +#endif // SHERPA_ONNX_PYTHON_CSRC_OFFLINE_PUNCTUATION_H_ diff --git a/sherpa-onnx/python/csrc/sherpa-onnx.cc b/sherpa-onnx/python/csrc/sherpa-onnx.cc index 31dd9bafd..242d85974 100644 --- a/sherpa-onnx/python/csrc/sherpa-onnx.cc +++ b/sherpa-onnx/python/csrc/sherpa-onnx.cc @@ -14,6 +14,7 @@ #include "sherpa-onnx/python/csrc/offline-ctc-fst-decoder-config.h" #include "sherpa-onnx/python/csrc/offline-lm-config.h" #include "sherpa-onnx/python/csrc/offline-model-config.h" +#include "sherpa-onnx/python/csrc/offline-punctuation.h" #include "sherpa-onnx/python/csrc/offline-recognizer.h" #include "sherpa-onnx/python/csrc/offline-stream.h" #include "sherpa-onnx/python/csrc/online-ctc-fst-decoder-config.h" @@ -40,6 +41,7 @@ PYBIND11_MODULE(_sherpa_onnx, m) { PybindWaveWriter(&m); PybindAudioTagging(&m); + PybindOfflinePunctuation(&m); PybindFeatures(&m); PybindOnlineCtcFstDecoderConfig(&m); diff --git a/sherpa-onnx/python/sherpa_onnx/__init__.py b/sherpa-onnx/python/sherpa_onnx/__init__.py index 2b1607312..7a832ba06 100644 --- a/sherpa-onnx/python/sherpa_onnx/__init__.py +++ b/sherpa-onnx/python/sherpa_onnx/__init__.py @@ -6,6 +6,9 @@ AudioTaggingModelConfig, CircularBuffer, Display, + OfflinePunctuation, + OfflinePunctuationConfig, + OfflinePunctuationModelConfig, OfflineStream, OfflineTts, OfflineTtsConfig, From b6ad0436fab983d992310964b3455cd69b3bbcad Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Sat, 13 Apr 2024 16:34:15 +0800 Subject: [PATCH 22/45] Release v1.9.18 (#763) --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 670b4b3e2..5f3ccc852 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.13 FATAL_ERROR) project(sherpa-onnx) -set(SHERPA_ONNX_VERSION "1.9.17") +set(SHERPA_ONNX_VERSION "1.9.18") # Disable warning about # From 983df28a83baba538e69f44acf8acdfd9342a574 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Sat, 13 Apr 2024 19:08:46 +0800 Subject: [PATCH 23/45] Fix a punctuation bug (#764) --- CMakeLists.txt | 2 +- .../offline-punctuation-ct-transformer-impl.h | 22 ++++++++----------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f3ccc852..d42cc8ba9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.13 FATAL_ERROR) project(sherpa-onnx) -set(SHERPA_ONNX_VERSION "1.9.18") +set(SHERPA_ONNX_VERSION "1.9.19") # Disable warning about # diff --git a/sherpa-onnx/csrc/offline-punctuation-ct-transformer-impl.h b/sherpa-onnx/csrc/offline-punctuation-ct-transformer-impl.h index 134b8807c..393feba9e 100644 --- a/sherpa-onnx/csrc/offline-punctuation-ct-transformer-impl.h +++ b/sherpa-onnx/csrc/offline-punctuation-ct-transformer-impl.h @@ -98,7 +98,7 @@ class OfflinePunctuationCtTransformerImpl : public OfflinePunctuationImpl { int32_t dot_index = -1; int32_t comma_index = -1; - for (int32_t m = this_punctuations.size() - 1; m >= 1; --m) { + for (int32_t m = this_punctuations.size() - 2; m >= 1; --m) { int32_t punct_id = this_punctuations[m]; if (punct_id == meta_data.dot_id || punct_id == meta_data.quest_id) { @@ -126,27 +126,20 @@ class OfflinePunctuationCtTransformerImpl : public OfflinePunctuationImpl { } } else { last = this_start + dot_index + 1; + } + if (dot_index != 1) { punctuations.insert(punctuations.end(), this_punctuations.begin(), this_punctuations.begin() + (dot_index + 1)); } } // for (int32_t i = 0; i != num_segments; ++i) - if (punctuations.size() != token_ids.size() && - punctuations.size() + 1 == token_ids.size()) { - punctuations.push_back(meta_data.dot_id); - } - - if (punctuations.size() != token_ids.size()) { - SHERPA_ONNX_LOGE("%s, %d, %d. Some unexpected things happened", - text.c_str(), static_cast(punctuations.size()), - static_cast(token_ids.size())); - return text; - } - std::string ans; for (int32_t i = 0; i != static_cast(punctuations.size()); ++i) { + if (i > tokens.size()) { + break; + } const std::string &w = tokens[i]; if (i > 0 && !(ans.back() & 0x80) && !(w[0] & 0x80)) { ans.push_back(' '); @@ -156,6 +149,9 @@ class OfflinePunctuationCtTransformerImpl : public OfflinePunctuationImpl { ans.append(meta_data.id2punct[punctuations[i]]); } } + if (ans.back() != meta_data.dot_id && ans.back() != meta_data.quest_id) { + ans.push_back(meta_data.dot_id); + } return ans; } From b0265b258dc9f868cdf02b8d74a6230d7b45788b Mon Sep 17 00:00:00 2001 From: gtf35 Date: Sat, 13 Apr 2024 23:39:07 +0800 Subject: [PATCH 24/45] Replace torchaudio with soundfile in python-api-examples (#765) --- ...aker-identification-with-vad-non-streaming-asr.py | 12 +++++++++--- .../speaker-identification-with-vad.py | 12 +++++++++--- python-api-examples/speaker-identification.py | 12 +++++++++--- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/python-api-examples/speaker-identification-with-vad-non-streaming-asr.py b/python-api-examples/speaker-identification-with-vad-non-streaming-asr.py index fe735e17a..0534b80f5 100755 --- a/python-api-examples/speaker-identification-with-vad-non-streaming-asr.py +++ b/python-api-examples/speaker-identification-with-vad-non-streaming-asr.py @@ -65,7 +65,7 @@ import numpy as np import sherpa_onnx -import torchaudio +import soundfile as sf try: import sounddevice as sd @@ -357,8 +357,14 @@ def load_speaker_file(args) -> Dict[str, List[str]]: def load_audio(filename: str) -> Tuple[np.ndarray, int]: - samples, sample_rate = torchaudio.load(filename) - return samples[0].contiguous().numpy(), sample_rate + data, sample_rate = sf.read( + filename, + always_2d=True, + dtype="float32", + ) + data = data[:, 0] # use only the first channel + samples = np.ascontiguousarray(data) + return samples, sample_rate def compute_speaker_embedding( diff --git a/python-api-examples/speaker-identification-with-vad.py b/python-api-examples/speaker-identification-with-vad.py index afad458dd..8514ed58f 100755 --- a/python-api-examples/speaker-identification-with-vad.py +++ b/python-api-examples/speaker-identification-with-vad.py @@ -60,7 +60,7 @@ import numpy as np import sherpa_onnx -import torchaudio +import soundfile as sf try: import sounddevice as sd @@ -160,8 +160,14 @@ def load_speaker_file(args) -> Dict[str, List[str]]: def load_audio(filename: str) -> Tuple[np.ndarray, int]: - samples, sample_rate = torchaudio.load(filename) - return samples[0].contiguous().numpy(), sample_rate + data, sample_rate = sf.read( + filename, + always_2d=True, + dtype="float32", + ) + data = data[:, 0] # use only the first channel + samples = np.ascontiguousarray(data) + return samples, sample_rate def compute_speaker_embedding( diff --git a/python-api-examples/speaker-identification.py b/python-api-examples/speaker-identification.py index c09478d81..abfa45587 100755 --- a/python-api-examples/speaker-identification.py +++ b/python-api-examples/speaker-identification.py @@ -52,7 +52,7 @@ import numpy as np import sherpa_onnx -import torchaudio +import soundfile as sf try: import sounddevice as sd @@ -145,8 +145,14 @@ def load_speaker_file(args) -> Dict[str, List[str]]: def load_audio(filename: str) -> Tuple[np.ndarray, int]: - samples, sample_rate = torchaudio.load(filename) - return samples[0].contiguous().numpy(), sample_rate + data, sample_rate = sf.read( + filename, + always_2d=True, + dtype="float32", + ) + data = data[:, 0] # use only the first channel + samples = np.ascontiguousarray(data) + return samples, sample_rate def compute_speaker_embedding( From 13730ecbd81f97fbe3ff4c2559bf968989824f47 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Sun, 14 Apr 2024 19:02:34 +0800 Subject: [PATCH 25/45] Add C API for punctuation (#768) --- .github/scripts/test-c-api.sh | 13 +++ .github/workflows/linux.yaml | 3 +- .github/workflows/macos.yaml | 1 + .github/workflows/windows-x64.yaml | 1 + .github/workflows/windows-x86.yaml | 1 + CMakeLists.txt | 2 +- c-api-examples/CMakeLists.txt | 3 + c-api-examples/add-punctuation-c-api.c | 67 +++++++++++ .../streaming-paraformer-asr-microphone.py | 105 ++++++++++++++++++ sherpa-onnx/c-api/c-api.cc | 46 ++++++++ sherpa-onnx/c-api/c-api.h | 35 ++++++ .../offline-punctuation-ct-transformer-impl.h | 33 ++++-- .../python/csrc/offline-punctuation.cc | 2 + 13 files changed, 301 insertions(+), 11 deletions(-) create mode 100644 c-api-examples/add-punctuation-c-api.c create mode 100755 python-api-examples/streaming-paraformer-asr-microphone.py diff --git a/.github/scripts/test-c-api.sh b/.github/scripts/test-c-api.sh index b29d1a0b4..ce2f6350d 100755 --- a/.github/scripts/test-c-api.sh +++ b/.github/scripts/test-c-api.sh @@ -11,8 +11,21 @@ log() { echo "SLID_EXE is $SLID_EXE" echo "SID_EXE is $SID_EXE" echo "AT_EXE is $AT_EXE" +echo "PUNCT_EXE is $PUNCT_EXE" echo "PATH: $PATH" +log "------------------------------------------------------------" +log "Test adding punctuations " +log "------------------------------------------------------------" + +curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/punctuation-models/sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +ls -lh +tar xf sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +ls -lh sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12 +rm sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +$PUNCT_EXE +rm -rf sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12 + log "------------------------------------------------------------" log "Test audio tagging " log "------------------------------------------------------------" diff --git a/.github/workflows/linux.yaml b/.github/workflows/linux.yaml index 0b8ba50f2..260b99af5 100644 --- a/.github/workflows/linux.yaml +++ b/.github/workflows/linux.yaml @@ -126,7 +126,7 @@ jobs: - uses: actions/upload-artifact@v4 with: name: release-${{ matrix.build_type }}-with-shared-lib-${{ matrix.shared_lib }}-with-tts-${{ matrix.with_tts }} - path: build/bin/* + path: install/* - name: Test offline punctuation shell: bash @@ -143,6 +143,7 @@ jobs: export SLID_EXE=spoken-language-identification-c-api export SID_EXE=speaker-identification-c-api export AT_EXE=audio-tagging-c-api + export PUNCT_EXE=add-punctuation-c-api .github/scripts/test-c-api.sh diff --git a/.github/workflows/macos.yaml b/.github/workflows/macos.yaml index ecb2f8359..e70ff11e1 100644 --- a/.github/workflows/macos.yaml +++ b/.github/workflows/macos.yaml @@ -122,6 +122,7 @@ jobs: export SLID_EXE=spoken-language-identification-c-api export SID_EXE=speaker-identification-c-api export AT_EXE=audio-tagging-c-api + export PUNCT_EXE=add-punctuation-c-api .github/scripts/test-c-api.sh diff --git a/.github/workflows/windows-x64.yaml b/.github/workflows/windows-x64.yaml index 55eedb374..d160e475e 100644 --- a/.github/workflows/windows-x64.yaml +++ b/.github/workflows/windows-x64.yaml @@ -89,6 +89,7 @@ jobs: export SLID_EXE=spoken-language-identification-c-api.exe export SID_EXE=speaker-identification-c-api.exe export AT_EXE=audio-tagging-c-api.exe + export PUNCT_EXE=add-punctuation-c-api.exe .github/scripts/test-c-api.sh diff --git a/.github/workflows/windows-x86.yaml b/.github/workflows/windows-x86.yaml index b579487ae..c476ab107 100644 --- a/.github/workflows/windows-x86.yaml +++ b/.github/workflows/windows-x86.yaml @@ -89,6 +89,7 @@ jobs: export SLID_EXE=spoken-language-identification-c-api.exe export SID_EXE=speaker-identification-c-api.exe export AT_EXE=audio-tagging-c-api.exe + export PUNCT_EXE=add-punctuation-c-api.exe .github/scripts/test-c-api.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index d42cc8ba9..8bacab02f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.13 FATAL_ERROR) project(sherpa-onnx) -set(SHERPA_ONNX_VERSION "1.9.19") +set(SHERPA_ONNX_VERSION "1.9.21") # Disable warning about # diff --git a/c-api-examples/CMakeLists.txt b/c-api-examples/CMakeLists.txt index 8d9bfe985..3aa445474 100644 --- a/c-api-examples/CMakeLists.txt +++ b/c-api-examples/CMakeLists.txt @@ -21,6 +21,9 @@ target_link_libraries(streaming-hlg-decode-file-c-api sherpa-onnx-c-api) add_executable(audio-tagging-c-api audio-tagging-c-api.c) target_link_libraries(audio-tagging-c-api sherpa-onnx-c-api) +add_executable(add-punctuation-c-api add-punctuation-c-api.c) +target_link_libraries(add-punctuation-c-api sherpa-onnx-c-api) + if(SHERPA_ONNX_HAS_ALSA) add_subdirectory(./asr-microphone-example) elseif((UNIX AND NOT APPLE) OR LINUX) diff --git a/c-api-examples/add-punctuation-c-api.c b/c-api-examples/add-punctuation-c-api.c new file mode 100644 index 000000000..9041e4ba2 --- /dev/null +++ b/c-api-examples/add-punctuation-c-api.c @@ -0,0 +1,67 @@ +// c-api-examples/add-punctuation-c-api.c +// +// Copyright (c) 2024 Xiaomi Corporation + +// We assume you have pre-downloaded the model files for testing +// from https://github.com/k2-fsa/sherpa-onnx/releases/tag/punctuation-models +// +// An example is given below: +// +// clang-format off +// +// wget https://github.com/k2-fsa/sherpa-onnx/releases/download/punctuation-models/sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +// tar xvf sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +// rm sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2 +// +// clang-format on + +#include +#include +#include + +#include "sherpa-onnx/c-api/c-api.h" + +int32_t main() { + SherpaOnnxOfflinePunctuationConfig config; + memset(&config, 0, sizeof(config)); + + // clang-format off + config.model.ct_transformer = "./sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12/model.onnx"; + // clang-format on + config.model.num_threads = 1; + config.model.debug = 1; + config.model.provider = "cpu"; + + const SherpaOnnxOfflinePunctuation *punct = + SherpaOnnxCreateOfflinePunctuation(&config); + if (!punct) { + fprintf(stderr, + "Failed to create OfflinePunctuation. Please check your config"); + return -1; + } + + const char *texts[] = { + "这是一个测试你好吗How are you我很好thank you are you ok谢谢你", + "我们都是木头人不会说话不会动", + "The African blogosphere is rapidly expanding bringing more voices " + "online in the form of commentaries opinions analyses rants and poetry", + }; + + int32_t n = sizeof(texts) / sizeof(const char *); + fprintf(stderr, "n: %d\n", n); + + fprintf(stderr, "--------------------\n"); + for (int32_t i = 0; i != n; ++i) { + const char *text_with_punct = + SherpaOfflinePunctuationAddPunct(punct, texts[i]); + + fprintf(stderr, "Input text: %s\n", texts[i]); + fprintf(stderr, "Output text: %s\n", text_with_punct); + SherpaOfflinePunctuationFreeText(text_with_punct); + fprintf(stderr, "--------------------\n"); + } + + SherpaOnnxDestroyOfflinePunctuation(punct); + + return 0; +}; diff --git a/python-api-examples/streaming-paraformer-asr-microphone.py b/python-api-examples/streaming-paraformer-asr-microphone.py new file mode 100755 index 000000000..ad5c8f703 --- /dev/null +++ b/python-api-examples/streaming-paraformer-asr-microphone.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 + +# Real-time speech recognition from a microphone with sherpa-onnx Python API +# with endpoint detection. +# This script uses a streaming paraformer +# +# Please refer to +# https://k2-fsa.github.io/sherpa/onnx/pretrained_models/online-paraformer/paraformer-models.html# +# to download pre-trained models + +import sys +from pathlib import Path + +try: + import sounddevice as sd +except ImportError: + print("Please install sounddevice first. You can use") + print() + print(" pip install sounddevice") + print() + print("to install it") + sys.exit(-1) + +import sherpa_onnx + + +def assert_file_exists(filename: str): + assert Path(filename).is_file(), ( + f"{filename} does not exist!\n" + "Please refer to " + "https://k2-fsa.github.io/sherpa/onnx/pretrained_models/online-paraformer/paraformer-models.html to download it" + ) + + +def create_recognizer(): + encoder = "./sherpa-onnx-streaming-paraformer-bilingual-zh-en/encoder.int8.onnx" + decoder = "./sherpa-onnx-streaming-paraformer-bilingual-zh-en/decoder.int8.onnx" + tokens = "./sherpa-onnx-streaming-paraformer-bilingual-zh-en/tokens.txt" + assert_file_exists(encoder) + assert_file_exists(decoder) + assert_file_exists(tokens) + recognizer = sherpa_onnx.OnlineRecognizer.from_paraformer( + tokens=tokens, + encoder=encoder, + decoder=decoder, + num_threads=1, + sample_rate=16000, + feature_dim=80, + enable_endpoint_detection=True, + rule1_min_trailing_silence=2.4, + rule2_min_trailing_silence=1.2, + rule3_min_utterance_length=300, # it essentially disables this rule + ) + return recognizer + + +def main(): + devices = sd.query_devices() + if len(devices) == 0: + print("No microphone devices found") + sys.exit(0) + + print(devices) + default_input_device_idx = sd.default.device[0] + print(f'Use default device: {devices[default_input_device_idx]["name"]}') + + recognizer = create_recognizer() + print("Started! Please speak") + + # The model is using 16 kHz, we use 48 kHz here to demonstrate that + # sherpa-onnx will do resampling inside. + sample_rate = 48000 + samples_per_read = int(0.1 * sample_rate) # 0.1 second = 100 ms + + stream = recognizer.create_stream() + + last_result = "" + segment_id = 0 + with sd.InputStream(channels=1, dtype="float32", samplerate=sample_rate) as s: + while True: + samples, _ = s.read(samples_per_read) # a blocking read + samples = samples.reshape(-1) + stream.accept_waveform(sample_rate, samples) + while recognizer.is_ready(stream): + recognizer.decode_stream(stream) + + is_endpoint = recognizer.is_endpoint(stream) + + result = recognizer.get_result(stream) + + if result and (last_result != result): + last_result = result + print("\r{}:{}".format(segment_id, result), end="", flush=True) + if is_endpoint: + if result: + print("\r{}:{}".format(segment_id, result), flush=True) + segment_id += 1 + recognizer.reset(stream) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\nCaught Ctrl + C. Exiting") diff --git a/sherpa-onnx/c-api/c-api.cc b/sherpa-onnx/c-api/c-api.cc index 995817a02..81643e863 100644 --- a/sherpa-onnx/c-api/c-api.cc +++ b/sherpa-onnx/c-api/c-api.cc @@ -15,6 +15,7 @@ #include "sherpa-onnx/csrc/display.h" #include "sherpa-onnx/csrc/keyword-spotter.h" #include "sherpa-onnx/csrc/macros.h" +#include "sherpa-onnx/csrc/offline-punctuation.h" #include "sherpa-onnx/csrc/offline-recognizer.h" #include "sherpa-onnx/csrc/online-recognizer.h" #include "sherpa-onnx/csrc/speaker-embedding-extractor.h" @@ -1299,3 +1300,48 @@ void SherpaOnnxAudioTaggingFreeResults( delete[] events; } + +struct SherpaOnnxOfflinePunctuation { + std::unique_ptr impl; +}; + +const SherpaOnnxOfflinePunctuation *SherpaOnnxCreateOfflinePunctuation( + const SherpaOnnxOfflinePunctuationConfig *config) { + sherpa_onnx::OfflinePunctuationConfig c; + c.model.ct_transformer = SHERPA_ONNX_OR(config->model.ct_transformer, ""); + c.model.num_threads = SHERPA_ONNX_OR(config->model.num_threads, 1); + c.model.debug = config->model.debug; + c.model.provider = SHERPA_ONNX_OR(config->model.provider, "cpu"); + + if (c.model.debug) { + SHERPA_ONNX_LOGE("%s\n", c.ToString().c_str()); + } + + if (!c.Validate()) { + SHERPA_ONNX_LOGE("Errors in config"); + return nullptr; + } + + SherpaOnnxOfflinePunctuation *punct = new SherpaOnnxOfflinePunctuation; + punct->impl = std::make_unique(c); + + return punct; +} + +void SherpaOnnxDestroyOfflinePunctuation( + const SherpaOnnxOfflinePunctuation *punct) { + delete punct; +} + +const char *SherpaOfflinePunctuationAddPunct( + const SherpaOnnxOfflinePunctuation *punct, const char *text) { + std::string text_with_punct = punct->impl->AddPunctuation(text); + + char *ans = new char[text_with_punct.size() + 1]; + std::copy(text_with_punct.begin(), text_with_punct.end(), ans); + ans[text_with_punct.size()] = 0; + + return ans; +} + +void SherpaOfflinePunctuationFreeText(const char *text) { delete[] text; } diff --git a/sherpa-onnx/c-api/c-api.h b/sherpa-onnx/c-api/c-api.h index 3833209ae..1faf4f711 100644 --- a/sherpa-onnx/c-api/c-api.h +++ b/sherpa-onnx/c-api/c-api.h @@ -1149,6 +1149,41 @@ SherpaOnnxAudioTaggingCompute(const SherpaOnnxAudioTagging *tagger, SHERPA_ONNX_API void SherpaOnnxAudioTaggingFreeResults( const SherpaOnnxAudioEvent *const *p); +// ============================================================ +// For punctuation +// ============================================================ + +SHERPA_ONNX_API typedef struct SherpaOnnxOfflinePunctuationModelConfig { + const char *ct_transformer; + int32_t num_threads; + int32_t debug; // true to print debug information of the model + const char *provider; +} SherpaOnnxOfflinePunctuationModelConfig; + +SHERPA_ONNX_API typedef struct SherpaOnnxOfflinePunctuationConfig { + SherpaOnnxOfflinePunctuationModelConfig model; +} SherpaOnnxOfflinePunctuationConfig; + +SHERPA_ONNX_API typedef struct SherpaOnnxOfflinePunctuation + SherpaOnnxOfflinePunctuation; + +// The user has to invoke SherpaOnnxDestroyOfflinePunctuation() +// to free the returned pointer to avoid memory leak +SHERPA_ONNX_API const SherpaOnnxOfflinePunctuation * +SherpaOnnxCreateOfflinePunctuation( + const SherpaOnnxOfflinePunctuationConfig *config); + +SHERPA_ONNX_API void SherpaOnnxDestroyOfflinePunctuation( + const SherpaOnnxOfflinePunctuation *punct); + +// Add punctuations to the input text. +// The user has to invoke SherpaOfflinePunctuationFreeText() +// to free the returned pointer to avoid memory leak +SHERPA_ONNX_API const char *SherpaOfflinePunctuationAddPunct( + const SherpaOnnxOfflinePunctuation *punct, const char *text); + +SHERPA_ONNX_API void SherpaOfflinePunctuationFreeText(const char *text); + #if defined(__GNUC__) #pragma GCC diagnostic pop #endif diff --git a/sherpa-onnx/csrc/offline-punctuation-ct-transformer-impl.h b/sherpa-onnx/csrc/offline-punctuation-ct-transformer-impl.h index 393feba9e..4414a5a8f 100644 --- a/sherpa-onnx/csrc/offline-punctuation-ct-transformer-impl.h +++ b/sherpa-onnx/csrc/offline-punctuation-ct-transformer-impl.h @@ -134,25 +134,40 @@ class OfflinePunctuationCtTransformerImpl : public OfflinePunctuationImpl { } } // for (int32_t i = 0; i != num_segments; ++i) - std::string ans; + if (punctuations.empty()) { + return text + meta_data.id2punct[meta_data.dot_id]; + } + std::vector words_punct; for (int32_t i = 0; i != static_cast(punctuations.size()); ++i) { - if (i > tokens.size()) { + if (i >= tokens.size()) { break; } - const std::string &w = tokens[i]; - if (i > 0 && !(ans.back() & 0x80) && !(w[0] & 0x80)) { - ans.push_back(' '); + std::string &w = tokens[i]; + if (i > 0 && !(words_punct.back()[0] & 0x80) && !(w[0] & 0x80)) { + words_punct.push_back(" "); } - ans.append(w); + words_punct.push_back(std::move(w)); + if (punctuations[i] != meta_data.underline_id) { - ans.append(meta_data.id2punct[punctuations[i]]); + words_punct.push_back(meta_data.id2punct[punctuations[i]]); } } - if (ans.back() != meta_data.dot_id && ans.back() != meta_data.quest_id) { - ans.push_back(meta_data.dot_id); + + if (words_punct.back() == meta_data.id2punct[meta_data.comma_id] || + words_punct.back() == meta_data.id2punct[meta_data.pause_id]) { + words_punct.back() = meta_data.id2punct[meta_data.dot_id]; + } + + if (words_punct.back() != meta_data.id2punct[meta_data.dot_id] && + words_punct.back() != meta_data.id2punct[meta_data.quest_id]) { + words_punct.push_back(meta_data.id2punct[meta_data.dot_id]); } + std::string ans; + for (const auto &w : words_punct) { + ans.append(w); + } return ans; } diff --git a/sherpa-onnx/python/csrc/offline-punctuation.cc b/sherpa-onnx/python/csrc/offline-punctuation.cc index 7d3ff86d8..0ff25903f 100644 --- a/sherpa-onnx/python/csrc/offline-punctuation.cc +++ b/sherpa-onnx/python/csrc/offline-punctuation.cc @@ -4,6 +4,8 @@ #include "sherpa-onnx/python/csrc/offline-punctuation.h" +#include + #include "sherpa-onnx/csrc/offline-punctuation.h" namespace sherpa_onnx { From 5981adf4548b1ddc9729a5d4f181dc706d93f935 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Mon, 15 Apr 2024 13:49:35 +0800 Subject: [PATCH 26/45] Add Kotlin API for audio tagging (#770) --- kotlin-api-examples/AudioTagging.kt | 95 ++++++++++++ kotlin-api-examples/Main.kt | 44 ++++++ kotlin-api-examples/OfflineStream.kt | 24 +++ kotlin-api-examples/run.sh | 139 +++++++++++------- sherpa-onnx/csrc/audio-tagging-impl.cc | 18 +++ sherpa-onnx/csrc/audio-tagging-impl.h | 10 ++ sherpa-onnx/csrc/audio-tagging-label-file.cc | 17 +++ sherpa-onnx/csrc/audio-tagging-label-file.h | 8 + .../csrc/audio-tagging-zipformer-impl.h | 19 +++ sherpa-onnx/csrc/audio-tagging.cc | 10 ++ sherpa-onnx/csrc/audio-tagging.h | 9 ++ sherpa-onnx/jni/AudioTagging.kt | 84 +++++++++++ sherpa-onnx/jni/CMakeLists.txt | 6 +- sherpa-onnx/jni/audio-tagging.cc | 126 ++++++++++++++++ sherpa-onnx/jni/common.h | 23 +++ sherpa-onnx/jni/jni.cc | 20 +-- sherpa-onnx/jni/offline-stream.cc | 25 ++++ 17 files changed, 611 insertions(+), 66 deletions(-) create mode 100644 kotlin-api-examples/AudioTagging.kt create mode 100644 kotlin-api-examples/OfflineStream.kt create mode 100644 sherpa-onnx/jni/AudioTagging.kt create mode 100644 sherpa-onnx/jni/audio-tagging.cc create mode 100644 sherpa-onnx/jni/common.h create mode 100644 sherpa-onnx/jni/offline-stream.cc diff --git a/kotlin-api-examples/AudioTagging.kt b/kotlin-api-examples/AudioTagging.kt new file mode 100644 index 000000000..621cfb553 --- /dev/null +++ b/kotlin-api-examples/AudioTagging.kt @@ -0,0 +1,95 @@ +package com.k2fsa.sherpa.onnx + +import android.content.res.AssetManager +import android.util.Log + +private val TAG = "sherpa-onnx" + +data class OfflineZipformerAudioTaggingModelConfig ( + val model: String, +) + +data class AudioTaggingModelConfig ( + var zipformer: OfflineZipformerAudioTaggingModelConfig, + var numThreads: Int = 1, + var debug: Boolean = false, + var provider: String = "cpu", +) + +data class AudioTaggingConfig ( + var model: AudioTaggingModelConfig, + var labels: String, + var topK: Int = 5, +) + +data class AudioEvent ( + val name: String, + val index: Int, + val prob: Float, +) + +class AudioTagging( + assetManager: AssetManager? = null, + config: AudioTaggingConfig, +) { + private var ptr: Long + + init { + ptr = if (assetManager != null) { + newFromAsset(assetManager, config) + } else { + newFromFile(config) + } + } + + protected fun finalize() { + if(ptr != 0L) { + delete(ptr) + ptr = 0 + } + } + + fun release() = finalize() + + fun createStream(): OfflineStream { + val p = createStream(ptr) + return OfflineStream(p) + } + + fun compute(stream: OfflineStream, topK: Int=-1): ArrayList { + var events :Array = compute(ptr, stream.ptr, topK) + val ans = ArrayList() + + for (e in events) { + val p :Array = e as Array + ans.add(AudioEvent( + name=p[0] as String, + index=p[1] as Int, + prob=p[2] as Float, + )) + } + + return ans + } + + private external fun newFromAsset( + assetManager: AssetManager, + config: AudioTaggingConfig, + ): Long + + private external fun newFromFile( + config: AudioTaggingConfig, + ): Long + + private external fun delete(ptr: Long) + + private external fun createStream(ptr: Long): Long + + private external fun compute(ptr: Long, streamPtr: Long, topK: Int): Array + + companion object { + init { + System.loadLibrary("sherpa-onnx-jni") + } + } +} diff --git a/kotlin-api-examples/Main.kt b/kotlin-api-examples/Main.kt index 4402c71cd..bc82c6993 100644 --- a/kotlin-api-examples/Main.kt +++ b/kotlin-api-examples/Main.kt @@ -7,12 +7,56 @@ fun callback(samples: FloatArray): Unit { } fun main() { + testAudioTagging() testSpeakerRecognition() testTts() testAsr("transducer") testAsr("zipformer2-ctc") } +fun testAudioTagging() { + val config = AudioTaggingConfig( + model=AudioTaggingModelConfig( + zipformer=OfflineZipformerAudioTaggingModelConfig( + model="./sherpa-onnx-zipformer-audio-tagging-2024-04-09/model.int8.onnx", + ), + numThreads=1, + debug=true, + provider="cpu", + ), + labels="./sherpa-onnx-zipformer-audio-tagging-2024-04-09/class_labels_indices.csv", + topK=5, + ) + val tagger = AudioTagging(assetManager=null, config=config) + + val testFiles = arrayOf( + "./sherpa-onnx-zipformer-audio-tagging-2024-04-09/test_wavs/1.wav", + "./sherpa-onnx-zipformer-audio-tagging-2024-04-09/test_wavs/2.wav", + "./sherpa-onnx-zipformer-audio-tagging-2024-04-09/test_wavs/3.wav", + "./sherpa-onnx-zipformer-audio-tagging-2024-04-09/test_wavs/4.wav", + ) + println("----------") + for (waveFilename in testFiles) { + val stream = tagger.createStream() + + val objArray = WaveReader.readWaveFromFile( + filename = waveFilename, + ) + val samples: FloatArray = objArray[0] as FloatArray + val sampleRate: Int = objArray[1] as Int + + stream.acceptWaveform(samples, sampleRate = sampleRate) + val events = tagger.compute(stream) + stream.release() + + println(waveFilename) + println(events) + println("----------") + } + + tagger.release() +} + fun computeEmbedding(extractor: SpeakerEmbeddingExtractor, filename: String): FloatArray { var objArray = WaveReader.readWaveFromFile( filename = filename, diff --git a/kotlin-api-examples/OfflineStream.kt b/kotlin-api-examples/OfflineStream.kt new file mode 100644 index 000000000..a4e650f80 --- /dev/null +++ b/kotlin-api-examples/OfflineStream.kt @@ -0,0 +1,24 @@ +package com.k2fsa.sherpa.onnx + +class OfflineStream(var ptr: Long) { + fun acceptWaveform(samples: FloatArray, sampleRate: Int) = + acceptWaveform(ptr, samples, sampleRate) + + protected fun finalize() { + if(ptr != 0L) { + delete(ptr) + ptr = 0 + } + } + + fun release() = finalize() + + private external fun acceptWaveform(ptr: Long, samples: FloatArray, sampleRate: Int) + private external fun delete(ptr: Long) + + companion object { + init { + System.loadLibrary("sherpa-onnx-jni") + } + } +} diff --git a/kotlin-api-examples/run.sh b/kotlin-api-examples/run.sh index 0a0b38d81..750a70e5c 100755 --- a/kotlin-api-examples/run.sh +++ b/kotlin-api-examples/run.sh @@ -4,8 +4,7 @@ # Note: This scripts runs only on Linux and macOS, though sherpa-onnx # supports building JNI libs for Windows. -set -e - +set -ex cd .. mkdir -p build @@ -29,59 +28,93 @@ export LD_LIBRARY_PATH=$PWD/build/lib:$LD_LIBRARY_PATH cd ../kotlin-api-examples -if [ ! -f ./3dspeaker_speech_eres2net_large_sv_zh-cn_3dspeaker_16k.onnx ]; then - wget -q https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/3dspeaker_speech_eres2net_large_sv_zh-cn_3dspeaker_16k.onnx -fi - -if [ ! -f ./speaker1_a_cn_16k.wav ]; then - wget -q https://github.com/csukuangfj/sr-data/raw/main/test/3d-speaker/speaker1_a_cn_16k.wav -fi - -if [ ! -f ./speaker1_b_cn_16k.wav ]; then - wget -q https://github.com/csukuangfj/sr-data/raw/main/test/3d-speaker/speaker1_b_cn_16k.wav -fi - -if [ ! -f ./speaker2_a_cn_16k.wav ]; then - wget -q https://github.com/csukuangfj/sr-data/raw/main/test/3d-speaker/speaker2_a_cn_16k.wav -fi - -if [ ! -f ./sherpa-onnx-streaming-zipformer-en-2023-02-21/tokens.txt ]; then - git lfs install - git clone https://huggingface.co/csukuangfj/sherpa-onnx-streaming-zipformer-en-2023-02-21 -fi - -if [ ! -d ./sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13 ]; then - wget -q https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13.tar.bz2 - tar xvf sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13.tar.bz2 - rm sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13.tar.bz2 -fi - -if [ ! -f ./vits-piper-en_US-amy-low/en_US-amy-low.onnx ]; then - wget -q https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-piper-en_US-amy-low.tar.bz2 - tar xf vits-piper-en_US-amy-low.tar.bz2 - rm vits-piper-en_US-amy-low.tar.bz2 -fi - -kotlinc-jvm -include-runtime -d main.jar Main.kt WaveReader.kt SherpaOnnx.kt faked-asset-manager.kt Tts.kt Speaker.kt faked-log.kt +function testSpeakerEmbeddingExtractor() { + if [ ! -f ./3dspeaker_speech_eres2net_large_sv_zh-cn_3dspeaker_16k.onnx ]; then + wget -q https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/3dspeaker_speech_eres2net_large_sv_zh-cn_3dspeaker_16k.onnx + fi + + if [ ! -f ./speaker1_a_cn_16k.wav ]; then + wget -q https://github.com/csukuangfj/sr-data/raw/main/test/3d-speaker/speaker1_a_cn_16k.wav + fi + + if [ ! -f ./speaker1_b_cn_16k.wav ]; then + wget -q https://github.com/csukuangfj/sr-data/raw/main/test/3d-speaker/speaker1_b_cn_16k.wav + fi + + if [ ! -f ./speaker2_a_cn_16k.wav ]; then + wget -q https://github.com/csukuangfj/sr-data/raw/main/test/3d-speaker/speaker2_a_cn_16k.wav + fi +} + +function testAsr() { + if [ ! -f ./sherpa-onnx-streaming-zipformer-en-2023-02-21/tokens.txt ]; then + git lfs install + git clone https://huggingface.co/csukuangfj/sherpa-onnx-streaming-zipformer-en-2023-02-21 + fi + + if [ ! -d ./sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13 ]; then + wget -q https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13.tar.bz2 + tar xvf sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13.tar.bz2 + rm sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13.tar.bz2 + fi +} + +function testTts() { + if [ ! -f ./vits-piper-en_US-amy-low/en_US-amy-low.onnx ]; then + wget -q https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-piper-en_US-amy-low.tar.bz2 + tar xf vits-piper-en_US-amy-low.tar.bz2 + rm vits-piper-en_US-amy-low.tar.bz2 + fi +} + +function testAudioTagging() { + if [ ! -d sherpa-onnx-zipformer-audio-tagging-2024-04-09 ]; then + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/audio-tagging-models/sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 + tar xvf sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 + rm sherpa-onnx-zipformer-audio-tagging-2024-04-09.tar.bz2 + fi +} + +function test() { + testAudioTagging + testSpeakerEmbeddingExtractor + testAsr + testTts +} + +test + +kotlinc-jvm -include-runtime -d main.jar \ + AudioTagging.kt \ + Main.kt \ + OfflineStream.kt \ + SherpaOnnx.kt \ + Speaker.kt \ + Tts.kt \ + WaveReader.kt \ + faked-asset-manager.kt \ + faked-log.kt ls -lh main.jar java -Djava.library.path=../build/lib -jar main.jar -# For two-pass - -if [ ! -f ./sherpa-onnx-streaming-zipformer-en-20M-2023-02-17/encoder-epoch-99-avg-1.int8.onnx ]; then - wget https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-en-20M-2023-02-17.tar.bz2 - tar xvf sherpa-onnx-streaming-zipformer-en-20M-2023-02-17.tar.bz2 - rm sherpa-onnx-streaming-zipformer-en-20M-2023-02-17.tar.bz2 -fi - -if [ ! -f ./sherpa-onnx-whisper-tiny.en/tiny.en-encoder.int8.onnx ]; then - wget https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-whisper-tiny.en.tar.bz2 - tar xvf sherpa-onnx-whisper-tiny.en.tar.bz2 - rm sherpa-onnx-whisper-tiny.en.tar.bz2 -fi - -kotlinc-jvm -include-runtime -d 2pass.jar test-2pass.kt WaveReader.kt SherpaOnnx2Pass.kt faked-asset-manager.kt -ls -lh 2pass.jar -java -Djava.library.path=../build/lib -jar 2pass.jar +function testTwoPass() { + if [ ! -f ./sherpa-onnx-streaming-zipformer-en-20M-2023-02-17/encoder-epoch-99-avg-1.int8.onnx ]; then + wget https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-en-20M-2023-02-17.tar.bz2 + tar xvf sherpa-onnx-streaming-zipformer-en-20M-2023-02-17.tar.bz2 + rm sherpa-onnx-streaming-zipformer-en-20M-2023-02-17.tar.bz2 + fi + + if [ ! -f ./sherpa-onnx-whisper-tiny.en/tiny.en-encoder.int8.onnx ]; then + wget https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-whisper-tiny.en.tar.bz2 + tar xvf sherpa-onnx-whisper-tiny.en.tar.bz2 + rm sherpa-onnx-whisper-tiny.en.tar.bz2 + fi + + kotlinc-jvm -include-runtime -d 2pass.jar test-2pass.kt WaveReader.kt SherpaOnnx2Pass.kt faked-asset-manager.kt + ls -lh 2pass.jar + java -Djava.library.path=../build/lib -jar 2pass.jar +} + +testTwoPass diff --git a/sherpa-onnx/csrc/audio-tagging-impl.cc b/sherpa-onnx/csrc/audio-tagging-impl.cc index 33e8dbb78..37cd6faa7 100644 --- a/sherpa-onnx/csrc/audio-tagging-impl.cc +++ b/sherpa-onnx/csrc/audio-tagging-impl.cc @@ -4,6 +4,11 @@ #include "sherpa-onnx/csrc/audio-tagging-impl.h" +#if __ANDROID_API__ >= 9 +#include "android/asset_manager.h" +#include "android/asset_manager_jni.h" +#endif + #include "sherpa-onnx/csrc/audio-tagging-zipformer-impl.h" #include "sherpa-onnx/csrc/macros.h" @@ -20,4 +25,17 @@ std::unique_ptr AudioTaggingImpl::Create( return nullptr; } +#if __ANDROID_API__ >= 9 +std::unique_ptr AudioTaggingImpl::Create( + AAssetManager *mgr, const AudioTaggingConfig &config) { + if (!config.model.zipformer.model.empty()) { + return std::make_unique(mgr, config); + } + + SHERPA_ONNX_LOG( + "Please specify an audio tagging model! Return a null pointer"); + return nullptr; +} +#endif + } // namespace sherpa_onnx diff --git a/sherpa-onnx/csrc/audio-tagging-impl.h b/sherpa-onnx/csrc/audio-tagging-impl.h index e5e192457..ac6f2e50e 100644 --- a/sherpa-onnx/csrc/audio-tagging-impl.h +++ b/sherpa-onnx/csrc/audio-tagging-impl.h @@ -7,6 +7,11 @@ #include #include +#if __ANDROID_API__ >= 9 +#include "android/asset_manager.h" +#include "android/asset_manager_jni.h" +#endif + #include "sherpa-onnx/csrc/audio-tagging.h" namespace sherpa_onnx { @@ -18,6 +23,11 @@ class AudioTaggingImpl { static std::unique_ptr Create( const AudioTaggingConfig &config); +#if __ANDROID_API__ >= 9 + static std::unique_ptr Create( + AAssetManager *mgr, const AudioTaggingConfig &config); +#endif + virtual std::unique_ptr CreateStream() const = 0; virtual std::vector Compute(OfflineStream *s, diff --git a/sherpa-onnx/csrc/audio-tagging-label-file.cc b/sherpa-onnx/csrc/audio-tagging-label-file.cc index 24846a174..f81e9670b 100644 --- a/sherpa-onnx/csrc/audio-tagging-label-file.cc +++ b/sherpa-onnx/csrc/audio-tagging-label-file.cc @@ -8,7 +8,15 @@ #include #include +#if __ANDROID_API__ >= 9 +#include + +#include "android/asset_manager.h" +#include "android/asset_manager_jni.h" +#endif + #include "sherpa-onnx/csrc/macros.h" +#include "sherpa-onnx/csrc/onnx-utils.h" #include "sherpa-onnx/csrc/text-utils.h" namespace sherpa_onnx { @@ -18,6 +26,15 @@ AudioTaggingLabels::AudioTaggingLabels(const std::string &filename) { Init(is); } +#if __ANDROID_API__ >= 9 +AudioTaggingLabels::AudioTaggingLabels(AAssetManager *mgr, + const std::string &filename) { + auto buf = ReadFile(mgr, filename); + std::istrstream is(buf.data(), buf.size()); + Init(is); +} +#endif + // Format of a label file /* index,mid,display_name diff --git a/sherpa-onnx/csrc/audio-tagging-label-file.h b/sherpa-onnx/csrc/audio-tagging-label-file.h index 9e71557f5..c366972eb 100644 --- a/sherpa-onnx/csrc/audio-tagging-label-file.h +++ b/sherpa-onnx/csrc/audio-tagging-label-file.h @@ -8,11 +8,19 @@ #include #include +#if __ANDROID_API__ >= 9 +#include "android/asset_manager.h" +#include "android/asset_manager_jni.h" +#endif + namespace sherpa_onnx { class AudioTaggingLabels { public: explicit AudioTaggingLabels(const std::string &filename); +#if __ANDROID_API__ >= 9 + AudioTaggingLabels(AAssetManager *mgr, const std::string &filename); +#endif // Return the event name for the given index. // The returned reference is valid as long as this object is alive diff --git a/sherpa-onnx/csrc/audio-tagging-zipformer-impl.h b/sherpa-onnx/csrc/audio-tagging-zipformer-impl.h index 639f644c8..65870dccf 100644 --- a/sherpa-onnx/csrc/audio-tagging-zipformer-impl.h +++ b/sherpa-onnx/csrc/audio-tagging-zipformer-impl.h @@ -8,6 +8,11 @@ #include #include +#if __ANDROID_API__ >= 9 +#include "android/asset_manager.h" +#include "android/asset_manager_jni.h" +#endif + #include "sherpa-onnx/csrc/audio-tagging-impl.h" #include "sherpa-onnx/csrc/audio-tagging-label-file.h" #include "sherpa-onnx/csrc/audio-tagging.h" @@ -28,6 +33,20 @@ class AudioTaggingZipformerImpl : public AudioTaggingImpl { } } +#if __ANDROID_API__ >= 9 + explicit AudioTaggingZipformerImpl(AAssetManager *mgr, + const AudioTaggingConfig &config) + : config_(config), + model_(mgr, config.model), + labels_(mgr, config.labels) { + if (model_.NumEventClasses() != labels_.NumEventClasses()) { + SHERPA_ONNX_LOGE("number of classes: %d (model) != %d (label file)", + model_.NumEventClasses(), labels_.NumEventClasses()); + exit(-1); + } + } +#endif + std::unique_ptr CreateStream() const override { return std::make_unique(); } diff --git a/sherpa-onnx/csrc/audio-tagging.cc b/sherpa-onnx/csrc/audio-tagging.cc index 34d558dd9..8fcb6ef45 100644 --- a/sherpa-onnx/csrc/audio-tagging.cc +++ b/sherpa-onnx/csrc/audio-tagging.cc @@ -4,6 +4,11 @@ #include "sherpa-onnx/csrc/audio-tagging.h" +#if __ANDROID_API__ >= 9 +#include "android/asset_manager.h" +#include "android/asset_manager_jni.h" +#endif + #include "sherpa-onnx/csrc/audio-tagging-impl.h" #include "sherpa-onnx/csrc/file-utils.h" #include "sherpa-onnx/csrc/macros.h" @@ -61,6 +66,11 @@ std::string AudioTaggingConfig::ToString() const { AudioTagging::AudioTagging(const AudioTaggingConfig &config) : impl_(AudioTaggingImpl::Create(config)) {} +#if __ANDROID_API__ >= 9 +AudioTagging::AudioTagging(AAssetManager *mgr, const AudioTaggingConfig &config) + : impl_(AudioTaggingImpl::Create(mgr, config)) {} +#endif + AudioTagging::~AudioTagging() = default; std::unique_ptr AudioTagging::CreateStream() const { diff --git a/sherpa-onnx/csrc/audio-tagging.h b/sherpa-onnx/csrc/audio-tagging.h index 50cfea02c..6f68e90f6 100644 --- a/sherpa-onnx/csrc/audio-tagging.h +++ b/sherpa-onnx/csrc/audio-tagging.h @@ -8,6 +8,11 @@ #include #include +#if __ANDROID_API__ >= 9 +#include "android/asset_manager.h" +#include "android/asset_manager_jni.h" +#endif + #include "sherpa-onnx/csrc/audio-tagging-model-config.h" #include "sherpa-onnx/csrc/offline-stream.h" #include "sherpa-onnx/csrc/parse-options.h" @@ -46,6 +51,10 @@ class AudioTagging { public: explicit AudioTagging(const AudioTaggingConfig &config); +#if __ANDROID_API__ >= 9 + AudioTagging(AAssetManager *mgr, const AudioTaggingConfig &config); +#endif + ~AudioTagging(); std::unique_ptr CreateStream() const; diff --git a/sherpa-onnx/jni/AudioTagging.kt b/sherpa-onnx/jni/AudioTagging.kt new file mode 100644 index 000000000..f3d827796 --- /dev/null +++ b/sherpa-onnx/jni/AudioTagging.kt @@ -0,0 +1,84 @@ +package com.k2fsa.sherpa.onnx + +import android.content.res.AssetManager +import android.util.Log + +private val TAG = "sherpa-onnx" + +data class OfflineZipformerAudioTaggingModelConfig ( + val model: String, +) + +data class AudioTaggingModelConfig ( + var zipformer: OfflineZipformerAudioTaggingModelConfig, + var numThreads: Int = 1, + var debug: Boolean = false, + var provider: String = "cpu", +) + +data class AudioTaggingConfig ( + var model: AudioTaggingModelConfig, + var labels: String, + var topK: Int = 5, +) + +data class AudioEvent ( + val name: String, + val index: Int, + val prob: Float, +) + +class AudioTagging( + assetManager: AssetManager? = null, + config: AudioTaggingConfig, +) { + private var ptr: Long + + init { + ptr = if (assetManager != null) { + newFromAsset(assetManager, config) + } else { + newFromFile(config) + } + } + + protected fun finalize() { + if(ptr != 0) { + delete(ptr) + ptr = 0 + } + } + + fun release() = finalize() + + fun createStream(): OfflineStream { + val p = createStream(ptr) + return OfflineStream(p) + } + + // fun compute(stream: OfflineStream, topK: Int=-1): Array { + fun compute(stream: OfflineStream, topK: Int=-1): Array { + var events :Array = compute(ptr, stream.ptr, topK) + } + + private external fun newFromAsset( + assetManager: AssetManager, + config: AudioTaggingConfig, + ): Long + + private external fun newFromFile( + config: AudioTaggingConfig, + ): Long + + private external fun delete(ptr: Long) + + private external fun createStream(ptr: Long): Long + + private external fun compute(ptr: Long, streamPtr: Long, topK: Int): Array + + companion object { + init { + System.loadLibrary("sherpa-onnx-jni") + } + } +} diff --git a/sherpa-onnx/jni/CMakeLists.txt b/sherpa-onnx/jni/CMakeLists.txt index 29e982113..75b6a1bb5 100644 --- a/sherpa-onnx/jni/CMakeLists.txt +++ b/sherpa-onnx/jni/CMakeLists.txt @@ -9,6 +9,10 @@ if(NOT DEFINED ANDROID_ABI) include_directories($ENV{JAVA_HOME}/include/darwin) endif() -add_library(sherpa-onnx-jni jni.cc) +add_library(sherpa-onnx-jni + audio-tagging.cc + jni.cc + offline-stream.cc +) target_link_libraries(sherpa-onnx-jni sherpa-onnx-core) install(TARGETS sherpa-onnx-jni DESTINATION lib) diff --git a/sherpa-onnx/jni/audio-tagging.cc b/sherpa-onnx/jni/audio-tagging.cc new file mode 100644 index 000000000..89fde8e58 --- /dev/null +++ b/sherpa-onnx/jni/audio-tagging.cc @@ -0,0 +1,126 @@ +// sherpa-onnx/jni/audio-tagging.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/csrc/audio-tagging.h" + +#include "sherpa-onnx/csrc/macros.h" +#include "sherpa-onnx/jni/common.h" + +namespace sherpa_onnx { + +static AudioTaggingConfig GetAudioTaggingConfig(JNIEnv *env, jobject config) { + AudioTaggingConfig ans; + + jclass cls = env->GetObjectClass(config); + + jfieldID fid = env->GetFieldID( + cls, "model", "Lcom/k2fsa/sherpa/onnx/AudioTaggingModelConfig;"); + jobject model = env->GetObjectField(config, fid); + jclass model_cls = env->GetObjectClass(model); + + fid = env->GetFieldID( + model_cls, "zipformer", + "Lcom/k2fsa/sherpa/onnx/OfflineZipformerAudioTaggingModelConfig;"); + jobject zipformer = env->GetObjectField(model, fid); + jclass zipformer_cls = env->GetObjectClass(zipformer); + + fid = env->GetFieldID(zipformer_cls, "model", "Ljava/lang/String;"); + jstring s = (jstring)env->GetObjectField(zipformer, fid); + const char *p = env->GetStringUTFChars(s, nullptr); + ans.model.zipformer.model = p; + env->ReleaseStringUTFChars(s, p); + + fid = env->GetFieldID(model_cls, "numThreads", "I"); + ans.model.num_threads = env->GetIntField(model, fid); + + fid = env->GetFieldID(model_cls, "debug", "Z"); + ans.model.debug = env->GetBooleanField(model, fid); + + fid = env->GetFieldID(model_cls, "provider", "Ljava/lang/String;"); + s = (jstring)env->GetObjectField(model, fid); + p = env->GetStringUTFChars(s, nullptr); + ans.model.provider = p; + env->ReleaseStringUTFChars(s, p); + + fid = env->GetFieldID(cls, "labels", "Ljava/lang/String;"); + s = (jstring)env->GetObjectField(config, fid); + p = env->GetStringUTFChars(s, nullptr); + ans.labels = p; + env->ReleaseStringUTFChars(s, p); + + fid = env->GetFieldID(cls, "topK", "I"); + ans.top_k = env->GetIntField(config, fid); + + return ans; +} + +} // namespace sherpa_onnx + +SHERPA_ONNX_EXTERN_C +JNIEXPORT jlong JNICALL Java_com_k2fsa_sherpa_onnx_AudioTagging_newFromFile( + JNIEnv *env, jobject /*obj*/, jobject _config) { + auto config = sherpa_onnx::GetAudioTaggingConfig(env, _config); + SHERPA_ONNX_LOGE("audio tagging newFromFile config:\n%s", + config.ToString().c_str()); + + if (!config.Validate()) { + SHERPA_ONNX_LOGE("Errors found in config!"); + return 0; + } + + auto tagger = new sherpa_onnx::AudioTagging(config); + + return (jlong)tagger; +} + +SHERPA_ONNX_EXTERN_C +JNIEXPORT void JNICALL Java_com_k2fsa_sherpa_onnx_AudioTagging_delete( + JNIEnv *env, jobject /*obj*/, jlong ptr) { + delete reinterpret_cast(ptr); +} + +SHERPA_ONNX_EXTERN_C +JNIEXPORT jlong JNICALL Java_com_k2fsa_sherpa_onnx_AudioTagging_createStream( + JNIEnv *env, jobject /*obj*/, jlong ptr) { + auto tagger = reinterpret_cast(ptr); + std::unique_ptr s = tagger->CreateStream(); + + // The user is responsible to free the returned pointer. + // + // See Java_com_k2fsa_sherpa_onnx_OfflineStream_delete() from + // ./offline-stream.cc + sherpa_onnx::OfflineStream *p = s.release(); + return (jlong)p; +} + +SHERPA_ONNX_EXTERN_C +JNIEXPORT jobjectArray JNICALL Java_com_k2fsa_sherpa_onnx_AudioTagging_compute( + JNIEnv *env, jobject /*obj*/, jlong ptr, jlong streamPtr, jint top_k) { + auto tagger = reinterpret_cast(ptr); + auto stream = reinterpret_cast(streamPtr); + std::vector events = tagger->Compute(stream, top_k); + + // TODO(fangjun): Return an array of AudioEvent directly + jobjectArray obj_arr = (jobjectArray)env->NewObjectArray( + events.size(), env->FindClass("java/lang/Object"), nullptr); + + int32_t i = 0; + for (const auto &e : events) { + jobjectArray a = (jobjectArray)env->NewObjectArray( + 3, env->FindClass("java/lang/Object"), nullptr); + + // 0 name + // 1 index + // 2 prob + jstring js = env->NewStringUTF(e.name.c_str()); + env->SetObjectArrayElement(a, 0, js); + env->SetObjectArrayElement(a, 1, NewInteger(env, e.index)); + env->SetObjectArrayElement(a, 2, NewFloat(env, e.prob)); + + env->SetObjectArrayElement(obj_arr, i, a); + i += 1; + } + + return obj_arr; +} diff --git a/sherpa-onnx/jni/common.h b/sherpa-onnx/jni/common.h new file mode 100644 index 000000000..d06350f86 --- /dev/null +++ b/sherpa-onnx/jni/common.h @@ -0,0 +1,23 @@ +// sherpa-onnx/jni/common.h +// +// Copyright (c) 2024 Xiaomi Corporation + +#ifndef SHERPA_ONNX_JNI_COMMON_H_ +#define SHERPA_ONNX_JNI_COMMON_H_ + +#if __ANDROID_API__ >= 9 +#include "android/asset_manager.h" +#include "android/asset_manager_jni.h" +#endif + +// If you use ndk, you can find "jni.h" inside +// android-ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include +#include "jni.h" // NOLINT + +#define SHERPA_ONNX_EXTERN_C extern "C" + +// defined in jni.cc +jobject NewInteger(JNIEnv *env, int32_t value); +jobject NewFloat(JNIEnv *env, float value); + +#endif // SHERPA_ONNX_JNI_COMMON_H_ diff --git a/sherpa-onnx/jni/jni.cc b/sherpa-onnx/jni/jni.cc index 23596e976..6bb25a362 100644 --- a/sherpa-onnx/jni/jni.cc +++ b/sherpa-onnx/jni/jni.cc @@ -7,20 +7,11 @@ // TODO(fangjun): Add documentation to functions/methods in this file // and also show how to use them with kotlin, possibly with java. -// If you use ndk, you can find "jni.h" inside -// android-ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include -#include "jni.h" // NOLINT - #include #include #include #include -#if __ANDROID_API__ >= 9 -#include "android/asset_manager.h" -#include "android/asset_manager_jni.h" -#endif - #include "sherpa-onnx/csrc/keyword-spotter.h" #include "sherpa-onnx/csrc/macros.h" #include "sherpa-onnx/csrc/offline-recognizer.h" @@ -31,13 +22,12 @@ #include "sherpa-onnx/csrc/voice-activity-detector.h" #include "sherpa-onnx/csrc/wave-reader.h" #include "sherpa-onnx/csrc/wave-writer.h" +#include "sherpa-onnx/jni/common.h" #if SHERPA_ONNX_ENABLE_TTS == 1 #include "sherpa-onnx/csrc/offline-tts.h" #endif -#define SHERPA_ONNX_EXTERN_C extern "C" - namespace sherpa_onnx { class SherpaOnnx { @@ -1224,12 +1214,18 @@ Java_com_k2fsa_sherpa_onnx_SpeakerEmbeddingManager_allSpeakerNames( // see // https://stackoverflow.com/questions/29043872/android-jni-return-multiple-variables -static jobject NewInteger(JNIEnv *env, int32_t value) { +jobject NewInteger(JNIEnv *env, int32_t value) { jclass cls = env->FindClass("java/lang/Integer"); jmethodID constructor = env->GetMethodID(cls, "", "(I)V"); return env->NewObject(cls, constructor, value); } +jobject NewFloat(JNIEnv *env, float value) { + jclass cls = env->FindClass("java/lang/Float"); + jmethodID constructor = env->GetMethodID(cls, "", "(F)V"); + return env->NewObject(cls, constructor, value); +} + #if SHERPA_ONNX_ENABLE_TTS == 1 SHERPA_ONNX_EXTERN_C JNIEXPORT jlong JNICALL Java_com_k2fsa_sherpa_onnx_OfflineTts_new( diff --git a/sherpa-onnx/jni/offline-stream.cc b/sherpa-onnx/jni/offline-stream.cc new file mode 100644 index 000000000..a2644d25e --- /dev/null +++ b/sherpa-onnx/jni/offline-stream.cc @@ -0,0 +1,25 @@ +// sherpa-onnx/jni/offline-stream.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/csrc/offline-stream.h" + +#include "sherpa-onnx/jni/common.h" + +SHERPA_ONNX_EXTERN_C +JNIEXPORT void JNICALL Java_com_k2fsa_sherpa_onnx_OfflineStream_delete( + JNIEnv *env, jobject /*obj*/, jlong ptr) { + delete reinterpret_cast(ptr); +} + +SHERPA_ONNX_EXTERN_C +JNIEXPORT void JNICALL Java_com_k2fsa_sherpa_onnx_OfflineStream_acceptWaveform( + JNIEnv *env, jobject /*obj*/, jlong ptr, jfloatArray samples, + jint sample_rate) { + auto stream = reinterpret_cast(ptr); + + jfloat *p = env->GetFloatArrayElements(samples, nullptr); + jsize n = env->GetArrayLength(samples); + stream->AcceptWaveform(sample_rate, p, n); + env->ReleaseFloatArrayElements(samples, p, JNI_ABORT); +} From fb4aee83ac501368e1877ce6df8d15131206a7bc Mon Sep 17 00:00:00 2001 From: Manix <50542248+manickavela29@users.noreply.github.com> Date: Tue, 16 Apr 2024 06:46:55 +0530 Subject: [PATCH 27/45] Adding warm up for Zipformer2 (#766) Signed-off-by: manickavela1998@gmail.com --- sherpa-onnx/csrc/online-model-config.cc | 5 +++ sherpa-onnx/csrc/online-model-config.h | 8 +++-- sherpa-onnx/csrc/online-recognizer-impl.h | 6 ++++ .../csrc/online-recognizer-transducer-impl.h | 36 +++++++++++++++++++ sherpa-onnx/csrc/online-recognizer.cc | 6 ++++ sherpa-onnx/csrc/online-recognizer.h | 9 +++++ .../csrc/online-websocket-server-impl.cc | 23 ++++++++++++ .../csrc/online-websocket-server-impl.h | 2 ++ .../python/csrc/online-model-config.cc | 10 +++--- 9 files changed, 99 insertions(+), 6 deletions(-) diff --git a/sherpa-onnx/csrc/online-model-config.cc b/sherpa-onnx/csrc/online-model-config.cc index 6e0ab6d77..16431c9b4 100644 --- a/sherpa-onnx/csrc/online-model-config.cc +++ b/sherpa-onnx/csrc/online-model-config.cc @@ -21,6 +21,10 @@ void OnlineModelConfig::Register(ParseOptions *po) { po->Register("num-threads", &num_threads, "Number of threads to run the neural network"); + po->Register("warm-up", &warm_up, + "Number of warm-up to run the onnxruntime" + "Valid vales are: zipformer2"); + po->Register("debug", &debug, "true to print model information while loading it."); @@ -70,6 +74,7 @@ std::string OnlineModelConfig::ToString() const { os << "zipformer2_ctc=" << zipformer2_ctc.ToString() << ", "; os << "tokens=\"" << tokens << "\", "; os << "num_threads=" << num_threads << ", "; + os << "warm_up=" << warm_up << ", "; os << "debug=" << (debug ? "True" : "False") << ", "; os << "provider=\"" << provider << "\", "; os << "model_type=\"" << model_type << "\")"; diff --git a/sherpa-onnx/csrc/online-model-config.h b/sherpa-onnx/csrc/online-model-config.h index bedabf119..d96168672 100644 --- a/sherpa-onnx/csrc/online-model-config.h +++ b/sherpa-onnx/csrc/online-model-config.h @@ -20,6 +20,7 @@ struct OnlineModelConfig { OnlineZipformer2CtcModelConfig zipformer2_ctc; std::string tokens; int32_t num_threads = 1; + int32_t warm_up = 0; bool debug = false; std::string provider = "cpu"; @@ -38,14 +39,17 @@ struct OnlineModelConfig { const OnlineParaformerModelConfig ¶former, const OnlineWenetCtcModelConfig &wenet_ctc, const OnlineZipformer2CtcModelConfig &zipformer2_ctc, - const std::string &tokens, int32_t num_threads, bool debug, - const std::string &provider, const std::string &model_type) + const std::string &tokens, int32_t num_threads, + int32_t warm_up, bool debug, + const std::string &provider, + const std::string &model_type) : transducer(transducer), paraformer(paraformer), wenet_ctc(wenet_ctc), zipformer2_ctc(zipformer2_ctc), tokens(tokens), num_threads(num_threads), + warm_up(warm_up), debug(debug), provider(provider), model_type(model_type) {} diff --git a/sherpa-onnx/csrc/online-recognizer-impl.h b/sherpa-onnx/csrc/online-recognizer-impl.h index db07ffa53..72efedec7 100644 --- a/sherpa-onnx/csrc/online-recognizer-impl.h +++ b/sherpa-onnx/csrc/online-recognizer-impl.h @@ -37,6 +37,12 @@ class OnlineRecognizerImpl { virtual bool IsReady(OnlineStream *s) const = 0; + virtual void WarmpUpRecognizer(int32_t warmup, int32_t mbs) const { + // ToDo extending to other models + SHERPA_ONNX_LOGE("Only zipformer2 model supports Warm up for now."); + exit(-1); + } + virtual void DecodeStreams(OnlineStream **ss, int32_t n) const = 0; virtual OnlineRecognizerResult GetResult(OnlineStream *s) const = 0; diff --git a/sherpa-onnx/csrc/online-recognizer-transducer-impl.h b/sherpa-onnx/csrc/online-recognizer-transducer-impl.h index 0fa3acac4..add0b85d6 100644 --- a/sherpa-onnx/csrc/online-recognizer-transducer-impl.h +++ b/sherpa-onnx/csrc/online-recognizer-transducer-impl.h @@ -32,6 +32,7 @@ #include "sherpa-onnx/csrc/online-transducer-modified-beam-search-decoder.h" #include "sherpa-onnx/csrc/symbol-table.h" #include "sherpa-onnx/csrc/utils.h" +#include "sherpa-onnx/csrc/onnx-utils.h" namespace sherpa_onnx { @@ -183,6 +184,41 @@ class OnlineRecognizerTransducerImpl : public OnlineRecognizerImpl { s->NumFramesReady(); } + // Warmping up engine with wp: warm_up count and max-batch-size + void WarmpUpRecognizer(int32_t warmup, int32_t mbs) const { + auto max_batch_size = mbs; + if (warmup <= 0 || warmup > 100) { + return; + } + int32_t chunk_size = model_->ChunkSize(); + int32_t chunk_shift = model_->ChunkShift(); + int32_t feature_dim = 80; + std::vector results(max_batch_size); + std::vector features_vec(max_batch_size * chunk_size * feature_dim); + std::vector> states_vec(max_batch_size); + + auto memory_info = + Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeDefault); + + std::array x_shape{max_batch_size, chunk_size, feature_dim}; + + for (int32_t i = 0; i != max_batch_size; ++i) { + states_vec[i] = model_->GetEncoderInitStates(); + results[i] = decoder_->GetEmptyResult(); + } + + for (int32_t i = 0; i != warmup; ++i) { + auto states = model_->StackStates(states_vec); + Ort::Value x = Ort::Value::CreateTensor(memory_info, features_vec.data(), + features_vec.size(), x_shape.data(), + x_shape.size()); + auto x_copy = Clone(model_->Allocator(), &x); + auto pair = model_->RunEncoder(std::move(x), std::move(states), + std::move(x_copy)); + decoder_->Decode(std::move(pair.first), &results); + } + } + void DecodeStreams(OnlineStream **ss, int32_t n) const override { int32_t chunk_size = model_->ChunkSize(); int32_t chunk_shift = model_->ChunkShift(); diff --git a/sherpa-onnx/csrc/online-recognizer.cc b/sherpa-onnx/csrc/online-recognizer.cc index 5d3445659..8bd0c16ad 100644 --- a/sherpa-onnx/csrc/online-recognizer.cc +++ b/sherpa-onnx/csrc/online-recognizer.cc @@ -171,6 +171,12 @@ bool OnlineRecognizer::IsReady(OnlineStream *s) const { return impl_->IsReady(s); } +void OnlineRecognizer::WarmpUpRecognizer(int32_t warmup, int32_t mbs) const { + if (warmup > 0) { + impl_->WarmpUpRecognizer(warmup, mbs); + } +} + void OnlineRecognizer::DecodeStreams(OnlineStream **ss, int32_t n) const { impl_->DecodeStreams(ss, n); } diff --git a/sherpa-onnx/csrc/online-recognizer.h b/sherpa-onnx/csrc/online-recognizer.h index e7f1b38d7..c1d2e9a74 100644 --- a/sherpa-onnx/csrc/online-recognizer.h +++ b/sherpa-onnx/csrc/online-recognizer.h @@ -162,6 +162,15 @@ class OnlineRecognizer { DecodeStreams(ss, 1); } + /** + * Warmups up onnxruntime sessions by apply optimization and + * allocating memory prior + * + * @param warmup Number of warmups. + * @param mbs : max-batch-size Max batch size for the models + */ + void WarmpUpRecognizer(int32_t warmup, int32_t mbs) const; + /** Decode multiple streams in parallel * * @param ss Pointer array containing streams to be decoded. diff --git a/sherpa-onnx/csrc/online-websocket-server-impl.cc b/sherpa-onnx/csrc/online-websocket-server-impl.cc index d02a4913e..11651f079 100644 --- a/sherpa-onnx/csrc/online-websocket-server-impl.cc +++ b/sherpa-onnx/csrc/online-websocket-server-impl.cc @@ -95,6 +95,11 @@ void OnlineWebsocketDecoder::InputFinished(std::shared_ptr c) { c->eof = true; } +void OnlineWebsocketDecoder::Warmup() const { + recognizer_->WarmpUpRecognizer(config_.recognizer_config.model_config.warm_up, + config_.max_batch_size); +} + void OnlineWebsocketDecoder::Run() { timer_.expires_after(std::chrono::milliseconds(config_.loop_interval_ms)); @@ -242,6 +247,24 @@ void OnlineWebsocketServer::Run(uint16_t port) { server_.set_reuse_addr(true); server_.listen(asio::ip::tcp::v4(), port); server_.start_accept(); + auto recognizer_config = config_.decoder_config.recognizer_config; + int32_t warm_up = recognizer_config.model_config.warm_up; + const std::string &model_type = recognizer_config.model_config.model_type; + if (0 < warm_up && warm_up < 100) { + if (model_type == "zipformer2") { + decoder_.Warmup(); + SHERPA_ONNX_LOGE("Warm up completed : %d times.", warm_up); + } else { + SHERPA_ONNX_LOGE("Only Zipformer2 has warmup support for now."); + SHERPA_ONNX_LOGE("Given: %s", model_type.c_str()); + exit(0); + } + } else if (warm_up == 0) { + SHERPA_ONNX_LOGE("Starting without warmup!"); + } else { + SHERPA_ONNX_LOGE("Invalid Warm up Value!. Expected 0 < warm_up < 100"); + exit(0); + } decoder_.Run(); } diff --git a/sherpa-onnx/csrc/online-websocket-server-impl.h b/sherpa-onnx/csrc/online-websocket-server-impl.h index 9716c5c72..4e0582dbc 100644 --- a/sherpa-onnx/csrc/online-websocket-server-impl.h +++ b/sherpa-onnx/csrc/online-websocket-server-impl.h @@ -85,6 +85,8 @@ class OnlineWebsocketDecoder { // signal that there will be no more audio samples for a stream void InputFinished(std::shared_ptr c); + void Warmup() const; + void Run(); private: diff --git a/sherpa-onnx/python/csrc/online-model-config.cc b/sherpa-onnx/python/csrc/online-model-config.cc index 9a8473510..2b4a8776b 100644 --- a/sherpa-onnx/python/csrc/online-model-config.cc +++ b/sherpa-onnx/python/csrc/online-model-config.cc @@ -27,14 +27,16 @@ void PybindOnlineModelConfig(py::module *m) { .def(py::init(), + const OnlineZipformer2CtcModelConfig &, + const std::string &, int32_t, int32_t, + bool, const std::string &, const std::string &>(), py::arg("transducer") = OnlineTransducerModelConfig(), py::arg("paraformer") = OnlineParaformerModelConfig(), py::arg("wenet_ctc") = OnlineWenetCtcModelConfig(), py::arg("zipformer2_ctc") = OnlineZipformer2CtcModelConfig(), - py::arg("tokens"), py::arg("num_threads"), py::arg("debug") = false, - py::arg("provider") = "cpu", py::arg("model_type") = "") + py::arg("tokens"), py::arg("num_threads"), py::arg("warm_up") = 0, + py::arg("debug") = false, py::arg("provider") = "cpu", + py::arg("model_type") = "") .def_readwrite("transducer", &PyClass::transducer) .def_readwrite("paraformer", &PyClass::paraformer) .def_readwrite("wenet_ctc", &PyClass::wenet_ctc) From 81b7f1d529cd4475b6d31e66335fd40ed0ac47bb Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Tue, 16 Apr 2024 09:17:23 +0800 Subject: [PATCH 28/45] Fix display for sherpa-onnx-microphone (#773) --- sherpa-onnx/csrc/sherpa-onnx-microphone.cc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sherpa-onnx/csrc/sherpa-onnx-microphone.cc b/sherpa-onnx/csrc/sherpa-onnx-microphone.cc index cb8e4d8d9..1b6760b57 100644 --- a/sherpa-onnx/csrc/sherpa-onnx-microphone.cc +++ b/sherpa-onnx/csrc/sherpa-onnx-microphone.cc @@ -148,7 +148,7 @@ for a list of pre-trained models to download. std::string last_text; int32_t segment_index = 0; - sherpa_onnx::Display display; + sherpa_onnx::Display display(30); while (!stop) { while (recognizer.IsReady(s.get())) { recognizer.DecodeStream(s.get()); @@ -163,14 +163,13 @@ for a list of pre-trained models to download. std::transform(text.begin(), text.end(), text.begin(), [](auto c) { return std::tolower(c); }); - fprintf(stderr, "\r%d: %s", segment_index, text.c_str()); + display.Print(segment_index, text); fflush(stderr); } if (is_endpoint) { if (!text.empty()) { ++segment_index; - fprintf(stderr, "\n"); } recognizer.Reset(s.get()); From 6bf2099781888f1c37ccebba66ceb36a98bd7a79 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Tue, 16 Apr 2024 09:46:15 +0800 Subject: [PATCH 29/45] Fix code style issues (#774) --- sherpa-onnx/csrc/features.cc | 5 ++--- sherpa-onnx/csrc/offline-lm-config.cc | 2 +- .../csrc/offline-recognizer-transducer-impl.h | 10 ++++------ .../offline-transducer-greedy-search-decoder.h | 3 +-- sherpa-onnx/csrc/offline-websocket-server-impl.cc | 6 +++--- sherpa-onnx/csrc/online-lm-config.cc | 2 +- sherpa-onnx/csrc/online-lm-config.h | 2 +- sherpa-onnx/csrc/online-model-config.h | 3 +-- .../csrc/online-recognizer-transducer-impl.h | 8 ++++---- sherpa-onnx/csrc/online-recognizer.h | 2 +- sherpa-onnx/csrc/online-rnn-lm.cc | 7 +++---- .../online-transducer-greedy-search-decoder.cc | 15 +++++++-------- .../online-transducer-greedy-search-decoder.h | 3 +-- sherpa-onnx/csrc/online-transducer-model.h | 2 +- ...ine-transducer-modified-beam-search-decoder.cc | 2 +- sherpa-onnx/csrc/stack-test.cc | 12 ++++++------ sherpa-onnx/python/csrc/features.cc | 6 ++---- sherpa-onnx/python/csrc/offline-recognizer.cc | 3 +-- .../csrc/offline-transducer-model-config.cc | 3 +-- sherpa-onnx/python/csrc/online-model-config.cc | 6 +++--- 20 files changed, 45 insertions(+), 57 deletions(-) diff --git a/sherpa-onnx/csrc/features.cc b/sherpa-onnx/csrc/features.cc index 7e510361a..c2d941e07 100644 --- a/sherpa-onnx/csrc/features.cc +++ b/sherpa-onnx/csrc/features.cc @@ -26,8 +26,7 @@ void FeatureExtractorConfig::Register(ParseOptions *po) { po->Register("feat-dim", &feature_dim, "Feature dimension. Must match the one expected by the model."); - po->Register("low-freq", &low_freq, - "Low cutoff frequency for mel bins"); + po->Register("low-freq", &low_freq, "Low cutoff frequency for mel bins"); po->Register("high-freq", &high_freq, "High cutoff frequency for mel bins " @@ -67,7 +66,7 @@ class FeatureExtractor::Impl { opts_.mel_opts.num_bins = config.feature_dim; opts_.mel_opts.high_freq = config.high_freq; - opts_.mel_opts.low_freq = config.low_freq; + opts_.mel_opts.low_freq = config.low_freq; opts_.mel_opts.is_librosa = config.is_librosa; diff --git a/sherpa-onnx/csrc/offline-lm-config.cc b/sherpa-onnx/csrc/offline-lm-config.cc index 262d91f01..078e56fab 100644 --- a/sherpa-onnx/csrc/offline-lm-config.cc +++ b/sherpa-onnx/csrc/offline-lm-config.cc @@ -15,7 +15,7 @@ void OfflineLMConfig::Register(ParseOptions *po) { po->Register("lm", &model, "Path to LM model."); po->Register("lm-scale", &scale, "LM scale."); po->Register("lm-num-threads", &lm_num_threads, - "Number of threads to run the neural network of LM model"); + "Number of threads to run the neural network of LM model"); po->Register("lm-provider", &lm_provider, "Specify a provider to LM model use: cpu, cuda, coreml"); } diff --git a/sherpa-onnx/csrc/offline-recognizer-transducer-impl.h b/sherpa-onnx/csrc/offline-recognizer-transducer-impl.h index de9f6263b..68ec63a3a 100644 --- a/sherpa-onnx/csrc/offline-recognizer-transducer-impl.h +++ b/sherpa-onnx/csrc/offline-recognizer-transducer-impl.h @@ -80,9 +80,8 @@ class OfflineRecognizerTransducerImpl : public OfflineRecognizerImpl { InitHotwords(); } if (config_.decoding_method == "greedy_search") { - decoder_ = - std::make_unique( - model_.get(), config_.blank_penalty); + decoder_ = std::make_unique( + model_.get(), config_.blank_penalty); } else if (config_.decoding_method == "modified_beam_search") { if (!config_.lm_config.model.empty()) { lm_ = OfflineLM::Create(config.lm_config); @@ -106,9 +105,8 @@ class OfflineRecognizerTransducerImpl : public OfflineRecognizerImpl { model_(std::make_unique(mgr, config_.model_config)) { if (config_.decoding_method == "greedy_search") { - decoder_ = - std::make_unique( - model_.get(), config_.blank_penalty); + decoder_ = std::make_unique( + model_.get(), config_.blank_penalty); } else if (config_.decoding_method == "modified_beam_search") { if (!config_.lm_config.model.empty()) { lm_ = OfflineLM::Create(mgr, config.lm_config); diff --git a/sherpa-onnx/csrc/offline-transducer-greedy-search-decoder.h b/sherpa-onnx/csrc/offline-transducer-greedy-search-decoder.h index f90ce9117..ca638c976 100644 --- a/sherpa-onnx/csrc/offline-transducer-greedy-search-decoder.h +++ b/sherpa-onnx/csrc/offline-transducer-greedy-search-decoder.h @@ -16,8 +16,7 @@ class OfflineTransducerGreedySearchDecoder : public OfflineTransducerDecoder { public: explicit OfflineTransducerGreedySearchDecoder(OfflineTransducerModel *model, float blank_penalty) - : model_(model), - blank_penalty_(blank_penalty) {} + : model_(model), blank_penalty_(blank_penalty) {} std::vector Decode( Ort::Value encoder_out, Ort::Value encoder_out_length, diff --git a/sherpa-onnx/csrc/offline-websocket-server-impl.cc b/sherpa-onnx/csrc/offline-websocket-server-impl.cc index d3f9310af..b34ebcaa9 100644 --- a/sherpa-onnx/csrc/offline-websocket-server-impl.cc +++ b/sherpa-onnx/csrc/offline-websocket-server-impl.cc @@ -102,9 +102,9 @@ void OfflineWebsocketDecoder::Decode() { asio::post(server_->GetConnectionContext(), [this, hdl, result = ss[i]->GetResult()]() { websocketpp::lib::error_code ec; - server_->GetServer().send( - hdl, result.AsJsonString(), - websocketpp::frame::opcode::text, ec); + server_->GetServer().send(hdl, result.AsJsonString(), + websocketpp::frame::opcode::text, + ec); if (ec) { server_->GetServer().get_alog().write( websocketpp::log::alevel::app, ec.message()); diff --git a/sherpa-onnx/csrc/online-lm-config.cc b/sherpa-onnx/csrc/online-lm-config.cc index d5b41d2b6..af75d1667 100644 --- a/sherpa-onnx/csrc/online-lm-config.cc +++ b/sherpa-onnx/csrc/online-lm-config.cc @@ -15,7 +15,7 @@ void OnlineLMConfig::Register(ParseOptions *po) { po->Register("lm", &model, "Path to LM model."); po->Register("lm-scale", &scale, "LM scale."); po->Register("lm-num-threads", &lm_num_threads, - "Number of threads to run the neural network of LM model"); + "Number of threads to run the neural network of LM model"); po->Register("lm-provider", &lm_provider, "Specify a provider to LM model use: cpu, cuda, coreml"); } diff --git a/sherpa-onnx/csrc/online-lm-config.h b/sherpa-onnx/csrc/online-lm-config.h index 90bc13d9e..16d7b0887 100644 --- a/sherpa-onnx/csrc/online-lm-config.h +++ b/sherpa-onnx/csrc/online-lm-config.h @@ -22,7 +22,7 @@ struct OnlineLMConfig { OnlineLMConfig() = default; OnlineLMConfig(const std::string &model, float scale, int32_t lm_num_threads, - const std::string &lm_provider) + const std::string &lm_provider) : model(model), scale(scale), lm_num_threads(lm_num_threads), diff --git a/sherpa-onnx/csrc/online-model-config.h b/sherpa-onnx/csrc/online-model-config.h index d96168672..3857ee426 100644 --- a/sherpa-onnx/csrc/online-model-config.h +++ b/sherpa-onnx/csrc/online-model-config.h @@ -40,8 +40,7 @@ struct OnlineModelConfig { const OnlineWenetCtcModelConfig &wenet_ctc, const OnlineZipformer2CtcModelConfig &zipformer2_ctc, const std::string &tokens, int32_t num_threads, - int32_t warm_up, bool debug, - const std::string &provider, + int32_t warm_up, bool debug, const std::string &provider, const std::string &model_type) : transducer(transducer), paraformer(paraformer), diff --git a/sherpa-onnx/csrc/online-recognizer-transducer-impl.h b/sherpa-onnx/csrc/online-recognizer-transducer-impl.h index add0b85d6..23fad3df1 100644 --- a/sherpa-onnx/csrc/online-recognizer-transducer-impl.h +++ b/sherpa-onnx/csrc/online-recognizer-transducer-impl.h @@ -30,9 +30,9 @@ #include "sherpa-onnx/csrc/online-transducer-greedy-search-decoder.h" #include "sherpa-onnx/csrc/online-transducer-model.h" #include "sherpa-onnx/csrc/online-transducer-modified-beam-search-decoder.h" +#include "sherpa-onnx/csrc/onnx-utils.h" #include "sherpa-onnx/csrc/symbol-table.h" #include "sherpa-onnx/csrc/utils.h" -#include "sherpa-onnx/csrc/onnx-utils.h" namespace sherpa_onnx { @@ -185,7 +185,7 @@ class OnlineRecognizerTransducerImpl : public OnlineRecognizerImpl { } // Warmping up engine with wp: warm_up count and max-batch-size - void WarmpUpRecognizer(int32_t warmup, int32_t mbs) const { + void WarmpUpRecognizer(int32_t warmup, int32_t mbs) const override { auto max_batch_size = mbs; if (warmup <= 0 || warmup > 100) { return; @@ -210,8 +210,8 @@ class OnlineRecognizerTransducerImpl : public OnlineRecognizerImpl { for (int32_t i = 0; i != warmup; ++i) { auto states = model_->StackStates(states_vec); Ort::Value x = Ort::Value::CreateTensor(memory_info, features_vec.data(), - features_vec.size(), x_shape.data(), - x_shape.size()); + features_vec.size(), + x_shape.data(), x_shape.size()); auto x_copy = Clone(model_->Allocator(), &x); auto pair = model_->RunEncoder(std::move(x), std::move(states), std::move(x_copy)); diff --git a/sherpa-onnx/csrc/online-recognizer.h b/sherpa-onnx/csrc/online-recognizer.h index c1d2e9a74..308cb08f7 100644 --- a/sherpa-onnx/csrc/online-recognizer.h +++ b/sherpa-onnx/csrc/online-recognizer.h @@ -168,7 +168,7 @@ class OnlineRecognizer { * * @param warmup Number of warmups. * @param mbs : max-batch-size Max batch size for the models - */ + */ void WarmpUpRecognizer(int32_t warmup, int32_t mbs) const; /** Decode multiple streams in parallel diff --git a/sherpa-onnx/csrc/online-rnn-lm.cc b/sherpa-onnx/csrc/online-rnn-lm.cc index ff493c930..5f938529b 100644 --- a/sherpa-onnx/csrc/online-rnn-lm.cc +++ b/sherpa-onnx/csrc/online-rnn-lm.cc @@ -12,8 +12,8 @@ #include "onnxruntime_cxx_api.h" // NOLINT #include "sherpa-onnx/csrc/macros.h" #include "sherpa-onnx/csrc/onnx-utils.h" -#include "sherpa-onnx/csrc/text-utils.h" #include "sherpa-onnx/csrc/session.h" +#include "sherpa-onnx/csrc/text-utils.h" namespace sherpa_onnx { @@ -42,10 +42,9 @@ class OnlineRnnLM::Impl { // nn_lm_scores std::array x_shape{1, 1}; Ort::Value x = Ort::Value::CreateTensor(allocator_, x_shape.data(), - x_shape.size()); + x_shape.size()); *x.GetTensorMutableData() = hyp->ys.back(); - auto lm_out = - ScoreToken(std::move(x), Convert(hyp->nn_lm_states)); + auto lm_out = ScoreToken(std::move(x), Convert(hyp->nn_lm_states)); hyp->nn_lm_scores.value = std::move(lm_out.first); hyp->nn_lm_states = Convert(std::move(lm_out.second)); } diff --git a/sherpa-onnx/csrc/online-transducer-greedy-search-decoder.cc b/sherpa-onnx/csrc/online-transducer-greedy-search-decoder.cc index c026e28a4..05523dbb3 100644 --- a/sherpa-onnx/csrc/online-transducer-greedy-search-decoder.cc +++ b/sherpa-onnx/csrc/online-transducer-greedy-search-decoder.cc @@ -71,11 +71,9 @@ void OnlineTransducerGreedySearchDecoder::StripLeadingBlanks( r->tokens = std::vector(start, end); } - void OnlineTransducerGreedySearchDecoder::Decode( Ort::Value encoder_out, std::vector *result) { - std::vector encoder_out_shape = encoder_out.GetTensorTypeAndShapeInfo().GetShape(); @@ -106,7 +104,8 @@ void OnlineTransducerGreedySearchDecoder::Decode( r.decoder_out.GetTensorTypeAndShapeInfo().GetShape(); decoder_out_shape[0] = batch_size; decoder_out = Ort::Value::CreateTensor(model_->Allocator(), - decoder_out_shape.data(), decoder_out_shape.size()); + decoder_out_shape.data(), + decoder_out_shape.size()); UseCachedDecoderOut(*result, &decoder_out); } else { Ort::Value decoder_input = model_->BuildDecoderInput(*result); @@ -116,8 +115,8 @@ void OnlineTransducerGreedySearchDecoder::Decode( for (int32_t t = 0; t != num_frames; ++t) { Ort::Value cur_encoder_out = GetEncoderOutFrame(model_->Allocator(), &encoder_out, t); - Ort::Value logit = model_->RunJoiner( - std::move(cur_encoder_out), View(&decoder_out)); + Ort::Value logit = + model_->RunJoiner(std::move(cur_encoder_out), View(&decoder_out)); float *p_logit = logit.GetTensorMutableData(); @@ -145,9 +144,9 @@ void OnlineTransducerGreedySearchDecoder::Decode( // export the per-token log scores if (y != 0 && y != unk_id_) { - LogSoftmax(p_logit, vocab_size); // renormalize probabilities, - // save time by doing it only for - // emitted symbols + LogSoftmax(p_logit, vocab_size); // renormalize probabilities, + // save time by doing it only for + // emitted symbols const float *p_logprob = p_logit; // rename p_logit as p_logprob, // now it contains normalized // probability diff --git a/sherpa-onnx/csrc/online-transducer-greedy-search-decoder.h b/sherpa-onnx/csrc/online-transducer-greedy-search-decoder.h index dd9faf8e8..c68c32dcf 100644 --- a/sherpa-onnx/csrc/online-transducer-greedy-search-decoder.h +++ b/sherpa-onnx/csrc/online-transducer-greedy-search-decoder.h @@ -15,8 +15,7 @@ namespace sherpa_onnx { class OnlineTransducerGreedySearchDecoder : public OnlineTransducerDecoder { public: OnlineTransducerGreedySearchDecoder(OnlineTransducerModel *model, - int32_t unk_id, - float blank_penalty) + int32_t unk_id, float blank_penalty) : model_(model), unk_id_(unk_id), blank_penalty_(blank_penalty) {} OnlineTransducerDecoderResult GetEmptyResult() const override; diff --git a/sherpa-onnx/csrc/online-transducer-model.h b/sherpa-onnx/csrc/online-transducer-model.h index bb763a932..3e248ec94 100644 --- a/sherpa-onnx/csrc/online-transducer-model.h +++ b/sherpa-onnx/csrc/online-transducer-model.h @@ -69,7 +69,7 @@ class OnlineTransducerModel { * This has to be called before GetEncoderInitStates(), so the `encoder_embed` * init state has the correct `embed_dim` of its output. */ - virtual void SetFeatureDim(int32_t feature_dim) { } + virtual void SetFeatureDim(int32_t feature_dim) {} /** Run the encoder. * diff --git a/sherpa-onnx/csrc/online-transducer-modified-beam-search-decoder.cc b/sherpa-onnx/csrc/online-transducer-modified-beam-search-decoder.cc index 5357974df..84fb46059 100644 --- a/sherpa-onnx/csrc/online-transducer-modified-beam-search-decoder.cc +++ b/sherpa-onnx/csrc/online-transducer-modified-beam-search-decoder.cc @@ -188,7 +188,7 @@ void OnlineTransducerModifiedBeamSearchDecoder::Decode( // score of the transducer // export the per-token log scores if (new_token != 0 && new_token != unk_id_) { - const Hypothesis& prev_i = prev[hyp_index]; + const Hypothesis &prev_i = prev[hyp_index]; // subtract 'prev[i]' path scores, which were added before // getting topk tokens float y_prob = p_logprob[k] - prev_i.log_prob - prev_i.lm_log_prob; diff --git a/sherpa-onnx/csrc/stack-test.cc b/sherpa-onnx/csrc/stack-test.cc index 45a8dfaae..519015b5c 100644 --- a/sherpa-onnx/csrc/stack-test.cc +++ b/sherpa-onnx/csrc/stack-test.cc @@ -16,10 +16,10 @@ TEST(Stack, Test1DTensors) { std::array b_shape{3}; Ort::Value a = Ort::Value::CreateTensor(allocator, a_shape.data(), - a_shape.size()); + a_shape.size()); Ort::Value b = Ort::Value::CreateTensor(allocator, b_shape.data(), - b_shape.size()); + b_shape.size()); float *pa = a.GetTensorMutableData(); float *pb = b.GetTensorMutableData(); for (int32_t i = 0; i != static_cast(a_shape[0]); ++i) { @@ -51,11 +51,11 @@ TEST(Stack, Test2DTensorsDim0) { std::array a_shape{2, 3}; std::array b_shape{2, 3}; - Ort::Value a = Ort::Value::CreateTensor( - allocator, a_shape.data(), a_shape.size()); + Ort::Value a = Ort::Value::CreateTensor(allocator, a_shape.data(), + a_shape.size()); - Ort::Value b = Ort::Value::CreateTensor( - allocator, b_shape.data(), b_shape.size()); + Ort::Value b = Ort::Value::CreateTensor(allocator, b_shape.data(), + b_shape.size()); float *pa = a.GetTensorMutableData(); float *pb = b.GetTensorMutableData(); diff --git a/sherpa-onnx/python/csrc/features.cc b/sherpa-onnx/python/csrc/features.cc index 333c6b675..4f179999a 100644 --- a/sherpa-onnx/python/csrc/features.cc +++ b/sherpa-onnx/python/csrc/features.cc @@ -12,10 +12,8 @@ static void PybindFeatureExtractorConfig(py::module *m) { using PyClass = FeatureExtractorConfig; py::class_(*m, "FeatureExtractorConfig") .def(py::init(), - py::arg("sampling_rate") = 16000, - py::arg("feature_dim") = 80, - py::arg("low_freq") = 20.0f, - py::arg("high_freq") = -400.0f, + py::arg("sampling_rate") = 16000, py::arg("feature_dim") = 80, + py::arg("low_freq") = 20.0f, py::arg("high_freq") = -400.0f, py::arg("dither") = 0.0f) .def_readwrite("sampling_rate", &PyClass::sampling_rate) .def_readwrite("feature_dim", &PyClass::feature_dim) diff --git a/sherpa-onnx/python/csrc/offline-recognizer.cc b/sherpa-onnx/python/csrc/offline-recognizer.cc index c0ebf7a82..823e280fa 100644 --- a/sherpa-onnx/python/csrc/offline-recognizer.cc +++ b/sherpa-onnx/python/csrc/offline-recognizer.cc @@ -23,8 +23,7 @@ static void PybindOfflineRecognizerConfig(py::module *m) { py::arg("ctc_fst_decoder_config") = OfflineCtcFstDecoderConfig(), py::arg("decoding_method") = "greedy_search", py::arg("max_active_paths") = 4, py::arg("hotwords_file") = "", - py::arg("hotwords_score") = 1.5, - py::arg("blank_penalty") = 0.0) + py::arg("hotwords_score") = 1.5, py::arg("blank_penalty") = 0.0) .def_readwrite("feat_config", &PyClass::feat_config) .def_readwrite("model_config", &PyClass::model_config) .def_readwrite("lm_config", &PyClass::lm_config) diff --git a/sherpa-onnx/python/csrc/offline-transducer-model-config.cc b/sherpa-onnx/python/csrc/offline-transducer-model-config.cc index 9d5999036..6d8f328ae 100644 --- a/sherpa-onnx/python/csrc/offline-transducer-model-config.cc +++ b/sherpa-onnx/python/csrc/offline-transducer-model-config.cc @@ -4,7 +4,6 @@ #include "sherpa-onnx/python/csrc/offline-transducer-model-config.h" - #include #include @@ -16,7 +15,7 @@ void PybindOfflineTransducerModelConfig(py::module *m) { using PyClass = OfflineTransducerModelConfig; py::class_(*m, "OfflineTransducerModelConfig") .def(py::init(), + const std::string &>(), py::arg("encoder_filename"), py::arg("decoder_filename"), py::arg("joiner_filename")) .def_readwrite("encoder_filename", &PyClass::encoder_filename) diff --git a/sherpa-onnx/python/csrc/online-model-config.cc b/sherpa-onnx/python/csrc/online-model-config.cc index 2b4a8776b..473be930f 100644 --- a/sherpa-onnx/python/csrc/online-model-config.cc +++ b/sherpa-onnx/python/csrc/online-model-config.cc @@ -27,9 +27,9 @@ void PybindOnlineModelConfig(py::module *m) { .def(py::init(), + const OnlineZipformer2CtcModelConfig &, const std::string &, + int32_t, int32_t, bool, const std::string &, + const std::string &>(), py::arg("transducer") = OnlineTransducerModelConfig(), py::arg("paraformer") = OnlineParaformerModelConfig(), py::arg("wenet_ctc") = OnlineWenetCtcModelConfig(), From aa2d695fd24846afb3c8b8c23b94783c3b85f313 Mon Sep 17 00:00:00 2001 From: chiiyeh <32455760+chiiyeh@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:29:46 +0800 Subject: [PATCH 30/45] Add score function to speaker identification (#775) --- sherpa-onnx/csrc/speaker-embedding-manager.cc | 22 +++++++++++++++++++ sherpa-onnx/csrc/speaker-embedding-manager.h | 2 ++ .../python/csrc/speaker-embedding-manager.cc | 8 +++++++ 3 files changed, 32 insertions(+) diff --git a/sherpa-onnx/csrc/speaker-embedding-manager.cc b/sherpa-onnx/csrc/speaker-embedding-manager.cc index e067a2eb7..f1c5251d6 100644 --- a/sherpa-onnx/csrc/speaker-embedding-manager.cc +++ b/sherpa-onnx/csrc/speaker-embedding-manager.cc @@ -151,6 +151,23 @@ class SpeakerEmbeddingManager::Impl { return true; } + float Score(const std::string &name, const float *p) { + if (!name2row_.count(name)) { + // Setting a default value if the name is not found + return -2.0; + } + + int32_t row_idx = name2row_.at(name); + + Eigen::VectorXf v = + Eigen::Map(const_cast(p), dim_); + v.normalize(); + + float score = embedding_matrix_.row(row_idx) * v; + + return score; + } + bool Contains(const std::string &name) const { return name2row_.count(name) > 0; } @@ -206,6 +223,11 @@ bool SpeakerEmbeddingManager::Verify(const std::string &name, const float *p, return impl_->Verify(name, p, threshold); } +float SpeakerEmbeddingManager::Score(const std::string &name, + const float *p) const { + return impl_->Score(name, p); +} + int32_t SpeakerEmbeddingManager::NumSpeakers() const { return impl_->NumSpeakers(); } diff --git a/sherpa-onnx/csrc/speaker-embedding-manager.h b/sherpa-onnx/csrc/speaker-embedding-manager.h index c1af12fc0..ae8728b13 100644 --- a/sherpa-onnx/csrc/speaker-embedding-manager.h +++ b/sherpa-onnx/csrc/speaker-embedding-manager.h @@ -74,6 +74,8 @@ class SpeakerEmbeddingManager { */ bool Verify(const std::string &name, const float *p, float threshold) const; + float Score(const std::string &name, const float *p) const; + // Return true if the given speaker already exists; return false otherwise. bool Contains(const std::string &name) const; diff --git a/sherpa-onnx/python/csrc/speaker-embedding-manager.cc b/sherpa-onnx/python/csrc/speaker-embedding-manager.cc index b1580ec16..b7bc4e174 100644 --- a/sherpa-onnx/python/csrc/speaker-embedding-manager.cc +++ b/sherpa-onnx/python/csrc/speaker-embedding-manager.cc @@ -60,6 +60,14 @@ void PybindSpeakerEmbeddingManager(py::module *m) { return self.Verify(name, v.data(), threshold); }, py::arg("name"), py::arg("v"), py::arg("threshold"), + py::call_guard()) + .def( + "score", + [](const PyClass &self, const std::string &name, + const std::vector &v) -> float { + return self.Score(name, v.data()); + }, + py::arg("name"), py::arg("v"), py::call_guard()); } From bcd9e48150d8bcaf67b10d681615ba039255c776 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Tue, 16 Apr 2024 20:47:16 +0800 Subject: [PATCH 31/45] Add Android demo for audio tagging (#776) See https://k2-fsa.github.io/sherpa/onnx/audio-tagging/apk.html --- .github/workflows/apk-audio-tagging.yaml | 174 ++++++++++++ .../workflows/apk-speaker-identification.yaml | 2 +- android/SherpaOnnxAudioTagging/.gitignore | 15 + android/SherpaOnnxAudioTagging/app/.gitignore | 1 + .../app/build.gradle.kts | 69 +++++ .../app/proguard-rules.pro | 21 ++ .../audio/tagging/ExampleInstrumentedTest.kt | 24 ++ .../app/src/main/AndroidManifest.xml | 30 ++ .../app/src/main/assets/.gitignore | 0 .../sherpa/onnx/audio/tagging/AudioTagging.kt | 137 ++++++++++ .../k2fsa/sherpa/onnx/audio/tagging/Home.kt | 256 ++++++++++++++++++ .../sherpa/onnx/audio/tagging/MainActivity.kt | 73 +++++ .../onnx/audio/tagging/OfflineStream.kt | 24 ++ .../k2fsa/sherpa/onnx/audio/tagging/Tagger.kt | 25 ++ .../onnx/audio/tagging/ui/theme/Color.kt | 11 + .../onnx/audio/tagging/ui/theme/Theme.kt | 70 +++++ .../onnx/audio/tagging/ui/theme/Type.kt | 34 +++ .../app/src/main/jniLibs/arm64-v8a/.gitignore | 0 .../src/main/jniLibs/armeabi-v7a/.gitignore | 0 .../app/src/main/jniLibs/x86/.gitignore | 0 .../app/src/main/jniLibs/x86_64/.gitignore | 0 .../drawable-v24/ic_launcher_foreground.xml | 30 ++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../app/src/main/res/values/colors.xml | 10 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/themes.xml | 5 + .../app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 ++ .../onnx/audio/tagging/ExampleUnitTest.kt | 17 ++ .../SherpaOnnxAudioTagging/build.gradle.kts | 5 + .../SherpaOnnxAudioTagging/gradle.properties | 23 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + android/SherpaOnnxAudioTagging/gradlew | 185 +++++++++++++ android/SherpaOnnxAudioTagging/gradlew.bat | 89 ++++++ .../settings.gradle.kts | 17 ++ kotlin-api-examples/AudioTagging.kt | 96 +------ kotlin-api-examples/OfflineStream.kt | 25 +- scripts/apk/build-apk-audio-tagging.sh.in | 87 ++++++ .../build-apk-speaker-identification.sh.in | 2 +- .../apk/generate-audio-tagging-apk-script.py | 93 +++++++ sherpa-onnx/jni/audio-tagging.cc | 23 ++ 54 files changed, 1775 insertions(+), 121 deletions(-) create mode 100644 .github/workflows/apk-audio-tagging.yaml create mode 100644 android/SherpaOnnxAudioTagging/.gitignore create mode 100644 android/SherpaOnnxAudioTagging/app/.gitignore create mode 100644 android/SherpaOnnxAudioTagging/app/build.gradle.kts create mode 100644 android/SherpaOnnxAudioTagging/app/proguard-rules.pro create mode 100644 android/SherpaOnnxAudioTagging/app/src/androidTest/java/com/k2fsa/sherpa/onnx/audio/tagging/ExampleInstrumentedTest.kt create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/AndroidManifest.xml create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/assets/.gitignore create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/AudioTagging.kt create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/Home.kt create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/MainActivity.kt create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/OfflineStream.kt create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/Tagger.kt create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/ui/theme/Color.kt create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/ui/theme/Theme.kt create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/ui/theme/Type.kt create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/jniLibs/arm64-v8a/.gitignore create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/jniLibs/armeabi-v7a/.gitignore create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/jniLibs/x86/.gitignore create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/jniLibs/x86_64/.gitignore create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/values/colors.xml create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/values/strings.xml create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/values/themes.xml create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/xml/backup_rules.xml create mode 100644 android/SherpaOnnxAudioTagging/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 android/SherpaOnnxAudioTagging/app/src/test/java/com/k2fsa/sherpa/onnx/audio/tagging/ExampleUnitTest.kt create mode 100644 android/SherpaOnnxAudioTagging/build.gradle.kts create mode 100644 android/SherpaOnnxAudioTagging/gradle.properties create mode 100644 android/SherpaOnnxAudioTagging/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/SherpaOnnxAudioTagging/gradle/wrapper/gradle-wrapper.properties create mode 100755 android/SherpaOnnxAudioTagging/gradlew create mode 100644 android/SherpaOnnxAudioTagging/gradlew.bat create mode 100644 android/SherpaOnnxAudioTagging/settings.gradle.kts mode change 100644 => 120000 kotlin-api-examples/AudioTagging.kt mode change 100644 => 120000 kotlin-api-examples/OfflineStream.kt create mode 100644 scripts/apk/build-apk-audio-tagging.sh.in create mode 100755 scripts/apk/generate-audio-tagging-apk-script.py diff --git a/.github/workflows/apk-audio-tagging.yaml b/.github/workflows/apk-audio-tagging.yaml new file mode 100644 index 000000000..8d18241fe --- /dev/null +++ b/.github/workflows/apk-audio-tagging.yaml @@ -0,0 +1,174 @@ +name: apk-audio-tagging + +on: + push: + tags: + - '*' + + workflow_dispatch: + +concurrency: + group: apk-audio-tagging-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + +jobs: + apk_audio_tagging: + if: github.repository_owner == 'csukuangfj' || github.repository_owner == 'k2-fsa' + runs-on: ${{ matrix.os }} + name: apk for audio tagging ${{ matrix.index }}/${{ matrix.total }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + total: ["1"] + index: ["0"] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # https://github.com/actions/setup-java + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '21' + + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: ${{ matrix.os }}-android + + - name: Display NDK HOME + shell: bash + run: | + echo "ANDROID_NDK_LATEST_HOME: ${ANDROID_NDK_LATEST_HOME}" + ls -lh ${ANDROID_NDK_LATEST_HOME} + + - name: Install Python dependencies + shell: bash + run: | + python3 -m pip install --upgrade pip jinja2 + + - name: Setup build tool version variable + shell: bash + run: | + echo "---" + ls -lh /usr/local/lib/android/ + echo "---" + + ls -lh /usr/local/lib/android/sdk + echo "---" + + ls -lh /usr/local/lib/android/sdk/build-tools + echo "---" + + BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1) + echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV + echo "Last build tool version is: $BUILD_TOOL_VERSION" + + - name: Generate build script + shell: bash + run: | + cd scripts/apk + + total=${{ matrix.total }} + index=${{ matrix.index }} + + ./generate-audio-tagging-apk-script.py --total $total --index $index + + chmod +x build-apk-audio-tagging.sh + mv -v ./build-apk-audio-tagging.sh ../.. + + - name: build APK + shell: bash + run: | + export CMAKE_CXX_COMPILER_LAUNCHER=ccache + export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" + cmake --version + + export ANDROID_NDK=$ANDROID_NDK_LATEST_HOME + ./build-apk-audio-tagging.sh + + - name: Display APK + shell: bash + run: | + ls -lh ./apks/ + du -h -d1 . + + # https://github.com/marketplace/actions/sign-android-release + - uses: r0adkll/sign-android-release@v1 + name: Sign app APK + with: + releaseDirectory: ./apks + signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} + alias: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }} + keyStorePassword: ${{ secrets.ANDROID_SIGNING_KEY_STORE_PASSWORD }} + env: + BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }} + + - name: Display APK for audio tagging after signing + shell: bash + run: | + ls -lh ./apks/ + du -h -d1 . + + - name: Rename APK for audio tagging after signing + shell: bash + run: | + cd apks + rm -fv signingKey.jks + rm -fv *.apk.idsig + rm -fv *-aligned.apk + + all_apks=$(ls -1 *-signed.apk) + echo "----" + echo $all_apks + echo "----" + for apk in ${all_apks[@]}; do + n=$(echo $apk | sed -e s/-signed//) + mv -v $apk $n + done + + cd .. + + ls -lh ./apks/ + du -h -d1 . + + - name: Display APK after rename + shell: bash + run: | + ls -lh ./apks/ + du -h -d1 . + + - name: Publish to huggingface + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + uses: nick-fields/retry@v3 + with: + max_attempts: 20 + timeout_seconds: 200 + shell: bash + command: | + git config --global user.email "csukuangfj@gmail.com" + git config --global user.name "Fangjun Kuang" + + rm -rf huggingface + export GIT_LFS_SKIP_SMUDGE=1 + + git clone https://huggingface.co/csukuangfj/sherpa-onnx-apk huggingface + cd huggingface + git fetch + git pull + git merge -m "merge remote" --ff origin main + + mkdir -p audio-tagging + cp -v ../apks/*.apk ./audio-tagging/ + git status + git lfs track "*.apk" + git add . + git commit -m "add more apks" + git push https://csukuangfj:$HF_TOKEN@huggingface.co/csukuangfj/sherpa-onnx-apk main diff --git a/.github/workflows/apk-speaker-identification.yaml b/.github/workflows/apk-speaker-identification.yaml index 7c21ab686..9a4bc2194 100644 --- a/.github/workflows/apk-speaker-identification.yaml +++ b/.github/workflows/apk-speaker-identification.yaml @@ -18,7 +18,7 @@ jobs: apk_speaker_identification: if: github.repository_owner == 'csukuangfj' || github.repository_owner == 'k2-fsa' runs-on: ${{ matrix.os }} - name: apk for tts ${{ matrix.index }}/${{ matrix.total }} + name: apk for speaker identification ${{ matrix.index }}/${{ matrix.total }} strategy: fail-fast: false matrix: diff --git a/android/SherpaOnnxAudioTagging/.gitignore b/android/SherpaOnnxAudioTagging/.gitignore new file mode 100644 index 000000000..aa724b770 --- /dev/null +++ b/android/SherpaOnnxAudioTagging/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/android/SherpaOnnxAudioTagging/app/.gitignore b/android/SherpaOnnxAudioTagging/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/build.gradle.kts b/android/SherpaOnnxAudioTagging/app/build.gradle.kts new file mode 100644 index 000000000..1709e2efa --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/build.gradle.kts @@ -0,0 +1,69 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.k2fsa.sherpa.onnx.audio.tagging" + compileSdk = 34 + + defaultConfig { + applicationId = "com.k2fsa.sherpa.onnx.audio.tagging" + minSdk = 21 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("androidx.activity:activity-compose:1.8.2") + implementation(platform("androidx.compose:compose-bom:2023.08.00")) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00")) + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") +} \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/proguard-rules.pro b/android/SherpaOnnxAudioTagging/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/src/androidTest/java/com/k2fsa/sherpa/onnx/audio/tagging/ExampleInstrumentedTest.kt b/android/SherpaOnnxAudioTagging/app/src/androidTest/java/com/k2fsa/sherpa/onnx/audio/tagging/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..c17852d6b --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/androidTest/java/com/k2fsa/sherpa/onnx/audio/tagging/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.k2fsa.sherpa.onnx.audio.tagging + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.k2fsa.sherpa.onnx.audio.tagging", appContext.packageName) + } +} \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/src/main/AndroidManifest.xml b/android/SherpaOnnxAudioTagging/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..3d205d4ea --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/src/main/assets/.gitignore b/android/SherpaOnnxAudioTagging/app/src/main/assets/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/AudioTagging.kt b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/AudioTagging.kt new file mode 100644 index 000000000..b3c850692 --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/AudioTagging.kt @@ -0,0 +1,137 @@ +package com.k2fsa.sherpa.onnx + +import android.content.res.AssetManager +import android.util.Log + +private val TAG = "sherpa-onnx" + +data class OfflineZipformerAudioTaggingModelConfig( + val model: String, +) + +data class AudioTaggingModelConfig( + var zipformer: OfflineZipformerAudioTaggingModelConfig, + var numThreads: Int = 1, + var debug: Boolean = false, + var provider: String = "cpu", +) + +data class AudioTaggingConfig( + var model: AudioTaggingModelConfig, + var labels: String, + var topK: Int = 5, +) + +data class AudioEvent( + val name: String, + val index: Int, + val prob: Float, +) + +class AudioTagging( + assetManager: AssetManager? = null, + config: AudioTaggingConfig, +) { + private var ptr: Long + + init { + ptr = if (assetManager != null) { + newFromAsset(assetManager, config) + } else { + newFromFile(config) + } + } + + protected fun finalize() { + if (ptr != 0L) { + delete(ptr) + ptr = 0 + } + } + + fun release() = finalize() + + fun createStream(): OfflineStream { + val p = createStream(ptr) + return OfflineStream(p) + } + + @Suppress("UNCHECKED_CAST") + fun compute(stream: OfflineStream, topK: Int = -1): ArrayList { + val events: Array = compute(ptr, stream.ptr, topK) + val ans = ArrayList() + + for (e in events) { + val p: Array = e as Array + ans.add( + AudioEvent( + name = p[0] as String, + index = p[1] as Int, + prob = p[2] as Float, + ) + ) + } + + return ans + } + + private external fun newFromAsset( + assetManager: AssetManager, + config: AudioTaggingConfig, + ): Long + + private external fun newFromFile( + config: AudioTaggingConfig, + ): Long + + private external fun delete(ptr: Long) + + private external fun createStream(ptr: Long): Long + + private external fun compute(ptr: Long, streamPtr: Long, topK: Int): Array + + companion object { + init { + System.loadLibrary("sherpa-onnx-jni") + } + } +} + +// please refer to +// https://github.com/k2-fsa/sherpa-onnx/releases/tag/audio-tagging-models +// to download more models +// +// See also +// https://k2-fsa.github.io/sherpa/onnx/audio-tagging/ +fun getAudioTaggingConfig(type: Int): AudioTaggingConfig? { + when (type) { + 0 -> { + val modelDir = "sherpa-onnx-zipformer-small-audio-tagging-2024-04-15" + return AudioTaggingConfig( + model = AudioTaggingModelConfig( + zipformer = OfflineZipformerAudioTaggingModelConfig(model = "$modelDir/model.int8.onnx"), + numThreads = 1, + debug = true, + ), + labels = "$modelDir/class_labels_indices.csv", + topK = 3, + ) + } + + 1 -> { + val modelDir = "sherpa-onnx-zipformer-audio-tagging-2024-04-09" + return AudioTaggingConfig( + model = AudioTaggingModelConfig( + zipformer = OfflineZipformerAudioTaggingModelConfig(model = "$modelDir/model.int8.onnx"), + numThreads = 1, + debug = true, + ), + labels = "$modelDir/class_labels_indices.csv", + topK = 3, + ) + } + + } + + return null +} \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/Home.kt b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/Home.kt new file mode 100644 index 000000000..0a84964a7 --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/Home.kt @@ -0,0 +1,256 @@ +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) + +package com.k2fsa.sherpa.onnx.audio.tagging + +import android.Manifest + +import android.app.Activity +import android.content.pm.PackageManager +import android.media.AudioFormat +import android.media.AudioRecord +import androidx.compose.foundation.lazy.items +import android.media.MediaRecorder +import android.util.Log +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Slider +import androidx.compose.material3.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.app.ActivityCompat +import com.k2fsa.sherpa.onnx.AudioEvent +import kotlin.concurrent.thread + + +@Composable +fun Home() { + Scaffold( + topBar = { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text( + "Next-gen Kaldi: Audio tagging", + fontWeight = FontWeight.Bold, + fontSize = 15.sp, + ) + }, + ) + }, + content = { + MyApp(it) + }, + ) +} + +private var audioRecord: AudioRecord? = null +private val sampleRateInHz = 16000 + +@Composable +fun MyApp(padding: PaddingValues) { + val activity = LocalContext.current as Activity + var threshold by remember { mutableStateOf(0.6F) } + var isStarted by remember { mutableStateOf(false) } + val result = remember { mutableStateListOf() } + + + val onButtonClick: () -> Unit = { + isStarted = !isStarted + if (isStarted) { + result.clear() + if (ActivityCompat.checkSelfPermission( + activity, + Manifest.permission.RECORD_AUDIO + ) != PackageManager.PERMISSION_GRANTED + ) { + Log.i(TAG, "Recording is not allowed") + } else { + val audioSource = MediaRecorder.AudioSource.MIC + val channelConfig = AudioFormat.CHANNEL_IN_MONO + val audioFormat = AudioFormat.ENCODING_PCM_16BIT + val numBytes = + AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat) + + audioRecord = AudioRecord( + audioSource, + sampleRateInHz, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT, + numBytes * 2 // a sample has two bytes as we are using 16-bit PCM + ) + + thread(true) { + Log.i(TAG, "processing samples") + val interval = 0.1 // i.e., 100 ms + val bufferSize = (interval * sampleRateInHz).toInt() // in samples + val buffer = ShortArray(bufferSize) + val sampleList = ArrayList() + audioRecord?.let { + it.startRecording() + while (isStarted) { + val ret = it.read(buffer, 0, buffer.size) + ret.let { n -> + val samples = FloatArray(n) { buffer[it] / 32768.0f } + sampleList.add(samples) + } + } + } + Log.i(TAG, "Stop recording") + Log.i(TAG, "Start recognition") + val samples = Flatten(sampleList) + val stream = Tagger.tagger.createStream() + stream.acceptWaveform(samples, sampleRateInHz) + val events = Tagger.tagger.compute(stream) + stream.release() + for (e in events) { + if (e.prob > threshold) { + result.add(e) + } + + } + + } + } + } + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter + ) { + Column( + Modifier.padding(padding), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(16.dp)) + Text("Threshold " + String.format("%.1f", threshold)) + Slider( + value = threshold, + onValueChange = { threshold = it }, + valueRange = 0.1F..1.0F, + modifier = Modifier.fillMaxWidth() + ) + + Button(onClick = onButtonClick) { + if (isStarted) { + Text("Stop") + } else { + Text("Start") + } + } + + Spacer(modifier = Modifier.height(16.dp)) + LazyColumn(modifier = Modifier.fillMaxSize()) { + if (!result.isEmpty()) { + + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Text( + text = "Event name", + ) + Text( + text = "Probability", + ) + } + } + } + + items(result) { event: AudioEvent -> + ViewRow(event = event) + } + } + } + } +} + +@Composable +fun ShowResult(result: String) { + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.primary, + text = result, + ) +} + +@Composable +fun ViewRow( + modifier: Modifier = Modifier, + event: AudioEvent +) { + Surface( + modifier = modifier + .fillMaxWidth() + .padding(8.dp), + color = MaterialTheme.colorScheme.inversePrimary, + ) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = event.name, + modifier = modifier.weight(1.0F), + ) + Text( + text = "%.2f".format(event.prob), + modifier = modifier.weight(1.0F), + ) + } + } +} + +fun Flatten(sampleList: ArrayList): FloatArray { + var totalSamples = 0 + for (a in sampleList) { + totalSamples += a.size + } + var i = 0 + val samples = FloatArray(totalSamples) + for (a in sampleList) { + for (s in a) { + samples[i] = s + i += 1 + } + } + Log.i(TAG, "$i, $totalSamples") + + return samples +} \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/MainActivity.kt b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/MainActivity.kt new file mode 100644 index 000000000..e76cff4cf --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/MainActivity.kt @@ -0,0 +1,73 @@ +package com.k2fsa.sherpa.onnx.audio.tagging + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.core.app.ActivityCompat +import com.k2fsa.sherpa.onnx.audio.tagging.ui.theme.SherpaOnnxAudioTaggingTheme + +const val TAG = "sherpa-onnx" + +private const val REQUEST_RECORD_AUDIO_PERMISSION = 200 + +class MainActivity : ComponentActivity() { + private val permissions: Array = arrayOf(Manifest.permission.RECORD_AUDIO) + override fun onCreate(savedInstanceState: Bundle?) { + + super.onCreate(savedInstanceState) + setContent { + AudioTaggingApp() + } + ActivityCompat.requestPermissions(this, permissions, REQUEST_RECORD_AUDIO_PERMISSION) + Tagger.initTagger(this.assets) + } + + @Suppress("DEPRECATION") + @Deprecated("Deprecated in Java") + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + val permissionToRecordAccepted = if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION) { + grantResults[0] == PackageManager.PERMISSION_GRANTED + } else { + false + } + + if (!permissionToRecordAccepted) { + Log.e(TAG, "Audio record is disallowed") + Toast.makeText( + this, + "This App needs access to the microphone", + Toast.LENGTH_SHORT + ) + .show() + finish() + } + Log.i(TAG, "Audio record is permitted") + } +} + +@Composable +fun AudioTaggingApp() { + SherpaOnnxAudioTaggingTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Home() + } + } +} \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/OfflineStream.kt b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/OfflineStream.kt new file mode 100644 index 000000000..49652e72d --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/OfflineStream.kt @@ -0,0 +1,24 @@ +package com.k2fsa.sherpa.onnx + +class OfflineStream(var ptr: Long) { + fun acceptWaveform(samples: FloatArray, sampleRate: Int) = + acceptWaveform(ptr, samples, sampleRate) + + protected fun finalize() { + if (ptr != 0L) { + delete(ptr) + ptr = 0 + } + } + + fun release() = finalize() + + private external fun acceptWaveform(ptr: Long, samples: FloatArray, sampleRate: Int) + private external fun delete(ptr: Long) + + companion object { + init { + System.loadLibrary("sherpa-onnx-jni") + } + } +} diff --git a/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/Tagger.kt b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/Tagger.kt new file mode 100644 index 000000000..8e6c970bc --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/Tagger.kt @@ -0,0 +1,25 @@ +package com.k2fsa.sherpa.onnx.audio.tagging + +import android.content.res.AssetManager +import android.util.Log +import com.k2fsa.sherpa.onnx.AudioTagging +import com.k2fsa.sherpa.onnx.getAudioTaggingConfig + +object Tagger { + private var _tagger: AudioTagging? = null + val tagger: AudioTagging + get() { + return _tagger!! + } + fun initTagger(assetManager: AssetManager? = null) { + synchronized(this) { + if (_tagger != null) { + return + } + + Log.i(TAG, "Initializing audio tagger") + val config = getAudioTaggingConfig(type = 0)!! + _tagger = AudioTagging(assetManager, config) + } + } +} \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/ui/theme/Color.kt b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/ui/theme/Color.kt new file mode 100644 index 000000000..fe96a8413 --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.k2fsa.sherpa.onnx.audio.tagging.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/ui/theme/Theme.kt b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/ui/theme/Theme.kt new file mode 100644 index 000000000..236dc8a51 --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/ui/theme/Theme.kt @@ -0,0 +1,70 @@ +package com.k2fsa.sherpa.onnx.audio.tagging.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun SherpaOnnxAudioTaggingTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/ui/theme/Type.kt b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/ui/theme/Type.kt new file mode 100644 index 000000000..e549c8bdc --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.k2fsa.sherpa.onnx.audio.tagging.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/src/main/jniLibs/arm64-v8a/.gitignore b/android/SherpaOnnxAudioTagging/app/src/main/jniLibs/arm64-v8a/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/android/SherpaOnnxAudioTagging/app/src/main/jniLibs/armeabi-v7a/.gitignore b/android/SherpaOnnxAudioTagging/app/src/main/jniLibs/armeabi-v7a/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/android/SherpaOnnxAudioTagging/app/src/main/jniLibs/x86/.gitignore b/android/SherpaOnnxAudioTagging/app/src/main/jniLibs/x86/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/android/SherpaOnnxAudioTagging/app/src/main/jniLibs/x86_64/.gitignore b/android/SherpaOnnxAudioTagging/app/src/main/jniLibs/x86_64/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/android/SherpaOnnxAudioTagging/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/SherpaOnnxAudioTagging/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/src/main/res/drawable/ic_launcher_background.xml b/android/SherpaOnnxAudioTagging/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 diff --git a/android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9 GIT binary patch literal 2898 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 literal 0 HcmV?d00001 diff --git a/android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i literal 0 HcmV?d00001 diff --git a/android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/SherpaOnnxAudioTagging/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f GIT binary patch literal 3918 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�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s literal 0 HcmV?d00001 diff --git a/android/SherpaOnnxAudioTagging/app/src/main/res/values/colors.xml b/android/SherpaOnnxAudioTagging/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..f8c6127d3 --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/src/main/res/values/strings.xml b/android/SherpaOnnxAudioTagging/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..be0de0b1c --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + SherpaOnnxAudioTagging + \ No newline at end of file diff --git a/android/SherpaOnnxAudioTagging/app/src/main/res/values/themes.xml b/android/SherpaOnnxAudioTagging/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..53b9432eb --- /dev/null +++ b/android/SherpaOnnxAudioTagging/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/SherpaOnnxAudioTaggingWearOs/build.gradle.kts b/android/SherpaOnnxAudioTaggingWearOs/build.gradle.kts new file mode 100644 index 000000000..8e8f4ab91 --- /dev/null +++ b/android/SherpaOnnxAudioTaggingWearOs/build.gradle.kts @@ -0,0 +1,5 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id("com.android.application") version "8.2.0" apply false + id("org.jetbrains.kotlin.android") version "1.9.0" apply false +} \ No newline at end of file diff --git a/android/SherpaOnnxAudioTaggingWearOs/gradle.properties b/android/SherpaOnnxAudioTaggingWearOs/gradle.properties new file mode 100644 index 000000000..3c5031eb7 --- /dev/null +++ b/android/SherpaOnnxAudioTaggingWearOs/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/android/SherpaOnnxAudioTaggingWearOs/gradle/wrapper/gradle-wrapper.jar b/android/SherpaOnnxAudioTaggingWearOs/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/android/SherpaOnnxAudioTaggingWearOs/gradle/wrapper/gradle-wrapper.properties b/android/SherpaOnnxAudioTaggingWearOs/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..9758ae8e7 --- /dev/null +++ b/android/SherpaOnnxAudioTaggingWearOs/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Apr 16 20:57:10 CST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/SherpaOnnxAudioTaggingWearOs/gradlew b/android/SherpaOnnxAudioTaggingWearOs/gradlew new file mode 100755 index 000000000..4f906e0c8 --- /dev/null +++ b/android/SherpaOnnxAudioTaggingWearOs/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/android/SherpaOnnxAudioTaggingWearOs/gradlew.bat b/android/SherpaOnnxAudioTaggingWearOs/gradlew.bat new file mode 100644 index 000000000..ac1b06f93 --- /dev/null +++ b/android/SherpaOnnxAudioTaggingWearOs/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/SherpaOnnxAudioTaggingWearOs/settings.gradle.kts b/android/SherpaOnnxAudioTaggingWearOs/settings.gradle.kts new file mode 100644 index 000000000..68476cb56 --- /dev/null +++ b/android/SherpaOnnxAudioTaggingWearOs/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "SherpaOnnxAudioTaggingWearOs" +include(":app") + \ No newline at end of file diff --git a/nodejs-examples/README.md b/nodejs-examples/README.md index 29c93a27d..4c3df2a6b 100644 --- a/nodejs-examples/README.md +++ b/nodejs-examples/README.md @@ -183,7 +183,7 @@ we use [sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18](https://github.com You can use the following command to run it: ```bash -wget https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 +wget -q https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 tar xvf sherpa-onnx-streaming-zipformer-ctc-small-2024-03-18.tar.bz2 node ./test-online-zipformer2-ctc-hlg.js ``` diff --git a/scripts/apk/build-apk-audio-tagging-wearos.sh.in b/scripts/apk/build-apk-audio-tagging-wearos.sh.in new file mode 100644 index 000000000..bc28f5268 --- /dev/null +++ b/scripts/apk/build-apk-audio-tagging-wearos.sh.in @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# +# Auto generated! Please DO NOT EDIT! + +# Please set the environment variable ANDROID_NDK +# before running this script + +# Inside the $ANDROID_NDK directory, you can find a binary ndk-build +# and some other files like the file "build/cmake/android.toolchain.cmake" + +set -ex + +log() { + # This function is from espnet + local fname=${BASH_SOURCE[1]##*/} + echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" +} + +SHERPA_ONNX_VERSION=$(grep "SHERPA_ONNX_VERSION" ./CMakeLists.txt | cut -d " " -f 2 | cut -d '"' -f 2) + +log "Building audio tagging WearOS APK for sherpa-onnx v${SHERPA_ONNX_VERSION}" + +log "====================arm64-v8a=================" +./build-android-arm64-v8a.sh +log "====================armv7-eabi================" +./build-android-armv7-eabi.sh +log "====================x86-64====================" +./build-android-x86-64.sh +log "====================x86====================" +./build-android-x86.sh + +mkdir -p apks + +{% for model in model_list %} +pushd ./android/SherpaOnnxAudioTaggingWearOs/app/src/main/assets/ +model_name={{ model.model_name }} +short_name={{ model.short_name }} +type={{ model.idx }} + +curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/audio-tagging-models/${model_name}.tar.bz2 +tar xvf ${model_name}.tar.bz2 +rm -rfv $model_name/model.onnx +rm -rfv $model_name/test_wavs +rm -rf *.tar.bz2 +ls -lh $model_name + +popd +# Now we are at the project root directory + +git checkout . +# Tagger.kt is a symlink file, so we use SherpaOnnxAudioTagging here instead of SherpaOnnxAudioTaggingWearOs +pushd android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/ +sed -i.bak s/"type = 0/type = $type/" ./Tagger.kt +git diff +popd + +for arch in arm64-v8a armeabi-v7a x86_64 x86; do + log "------------------------------------------------------------" + log "build audio tagging apk for $arch" + log "------------------------------------------------------------" + src_arch=$arch + if [ $arch == "armeabi-v7a" ]; then + src_arch=armv7-eabi + elif [ $arch == "x86_64" ]; then + src_arch=x86-64 + fi + + ls -lh ./build-android-$src_arch/install/lib/*.so + + cp -v ./build-android-$src_arch/install/lib/*.so ./android/SherpaOnnxAudioTaggingWearOs/app/src/main/jniLibs/$arch/ + + pushd ./android/SherpaOnnxAudioTaggingWearOs + sed -i.bak s/2048/9012/g ./gradle.properties + git diff ./gradle.properties + ./gradlew assembleRelease + popd + + mv android/SherpaOnnxAudioTaggingWearOs/app/build/outputs/apk/release/app-release-unsigned.apk ./apks/sherpa-onnx-${SHERPA_ONNX_VERSION}-$arch-audio-tagging-$short_name-wearos.apk + ls -lh apks + rm -v ./android/SherpaOnnxAudioTaggingWearOs/app/src/main/jniLibs/$arch/*.so +done + +rm -rf ./android/SherpaOnnxAudioTaggingWearOs/app/src/main/assets/$model_name +{% endfor %} + +git checkout . + +ls -lh apks/ diff --git a/scripts/apk/generate-audio-tagging-apk-script.py b/scripts/apk/generate-audio-tagging-apk-script.py index fdf21a9dc..e5855ad2c 100755 --- a/scripts/apk/generate-audio-tagging-apk-script.py +++ b/scripts/apk/generate-audio-tagging-apk-script.py @@ -77,7 +77,10 @@ def main(): d["model_list"].append(all_model_list[s]) print(f"{s}/{num_models}") - filename_list = ["./build-apk-audio-tagging.sh"] + filename_list = [ + "./build-apk-audio-tagging.sh", + "./build-apk-audio-tagging-wearos.sh", + ] for filename in filename_list: environment = jinja2.Environment() with open(f"{filename}.in") as f: diff --git a/scripts/dotnet/generate.py b/scripts/dotnet/generate.py index ed395f149..5268211b2 100755 --- a/scripts/dotnet/generate.py +++ b/scripts/dotnet/generate.py @@ -40,8 +40,8 @@ def process_linux(s): "libpiper_phonemize.so.1", "libsherpa-onnx-c-api.so", "libsherpa-onnx-core.so", - "libsherpa-onnx-fstfar.so.16", - "libsherpa-onnx-fst.so.16", + "libsherpa-onnx-fstfar.so.7", + "libsherpa-onnx-fst.so.6", "libsherpa-onnx-kaldifst-core.so", "libucd.so", ] @@ -69,8 +69,8 @@ def process_macos(s): "libpiper_phonemize.1.dylib", "libsherpa-onnx-c-api.dylib", "libsherpa-onnx-core.dylib", - "libsherpa-onnx-fstfar.16.dylib", - "libsherpa-onnx-fst.16.dylib", + "libsherpa-onnx-fstfar.7.dylib", + "libsherpa-onnx-fst.6.dylib", "libsherpa-onnx-kaldifst-core.dylib", "libucd.dylib", ] diff --git a/wasm/asr/assets/README.md b/wasm/asr/assets/README.md index 2d4bb5c83..d37c431a7 100644 --- a/wasm/asr/assets/README.md +++ b/wasm/asr/assets/README.md @@ -47,7 +47,7 @@ assets fangjun$ tree -L 1 ## Paraformer ``` -wget https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-paraformer-bilingual-zh-en.tar.bz2 +wget -q https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-paraformer-bilingual-zh-en.tar.bz2 tar xvf sherpa-onnx-streaming-paraformer-bilingual-zh-en.tar.bz2 rm sherpa-onnx-streaming-paraformer-bilingual-zh-en.tar.bz2 From 3a43049ba1171e354255a4622d15e088c2cc79fc Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Wed, 17 Apr 2024 19:27:15 +0800 Subject: [PATCH 33/45] Add JNI support for spoken language identification (#782) --- .github/workflows/test-go-package.yaml | 8 +- .gitignore | 1 + .../sherpa/onnx/audio/tagging/AudioTagging.kt | 4 +- kotlin-api-examples/Main.kt | 36 ++++++ .../SpokenLanguageIdentification.kt | 45 +++----- kotlin-api-examples/run.sh | 32 ++++-- sherpa-onnx/jni/CMakeLists.txt | 1 + .../jni/spoken-language-identification.cc | 104 ++++++++++++++++++ 8 files changed, 189 insertions(+), 42 deletions(-) rename sherpa-onnx/jni/AudioTagging.kt => kotlin-api-examples/SpokenLanguageIdentification.kt (53%) create mode 100644 sherpa-onnx/jni/spoken-language-identification.cc diff --git a/.github/workflows/test-go-package.yaml b/.github/workflows/test-go-package.yaml index 271329500..f76157ab2 100644 --- a/.github/workflows/test-go-package.yaml +++ b/.github/workflows/test-go-package.yaml @@ -161,10 +161,12 @@ jobs: ./run-vits-vctk.sh rm -rf vits-vctk - echo "Test vits-zh-aishell3" - git clone https://huggingface.co/csukuangfj/vits-zh-aishell3 + echo "Test vits-icefall-zh-aishell3" + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-icefall-zh-aishell3.tar.bz2 + tar xvf vits-icefall-zh-aishell3.tar.bz2 + rm vits-icefall-zh-aishell3.tar.bz2 ./run-vits-zh-aishell3.sh - rm -rf vits-zh-aishell3 + rm -rf vits-icefall-zh-aishell3* echo "Test vits-piper-en_US-lessac-medium" git clone https://huggingface.co/csukuangfj/vits-piper-en_US-lessac-medium diff --git a/.gitignore b/.gitignore index 3047a1e0a..e0743e07f 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,4 @@ sr-data vits-icefall-* sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12 +spoken-language-identification-test-wavs diff --git a/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/AudioTagging.kt b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/AudioTagging.kt index 9c4b5cebd..437302911 100644 --- a/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/AudioTagging.kt +++ b/android/SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/AudioTagging.kt @@ -6,7 +6,7 @@ import android.util.Log private val TAG = "sherpa-onnx" data class OfflineZipformerAudioTaggingModelConfig( - val model: String, + var model: String, ) data class AudioTaggingModelConfig( @@ -134,4 +134,4 @@ fun getAudioTaggingConfig(type: Int, numThreads: Int=1): AudioTaggingConfig? { } return null -} \ No newline at end of file +} diff --git a/kotlin-api-examples/Main.kt b/kotlin-api-examples/Main.kt index bc82c6993..479ce3428 100644 --- a/kotlin-api-examples/Main.kt +++ b/kotlin-api-examples/Main.kt @@ -7,6 +7,7 @@ fun callback(samples: FloatArray): Unit { } fun main() { + testSpokenLanguageIdentifcation() testAudioTagging() testSpeakerRecognition() testTts() @@ -14,6 +15,41 @@ fun main() { testAsr("zipformer2-ctc") } +fun testSpokenLanguageIdentifcation() { + val config = SpokenLanguageIdentificationConfig( + whisper = SpokenLanguageIdentificationWhisperConfig( + encoder = "./sherpa-onnx-whisper-tiny/tiny-encoder.int8.onnx", + decoder = "./sherpa-onnx-whisper-tiny/tiny-decoder.int8.onnx", + tailPaddings = 33, + ), + numThreads=1, + debug=true, + provider="cpu", + ) + val slid = SpokenLanguageIdentification(assetManager=null, config=config) + + val testFiles = arrayOf( + "./spoken-language-identification-test-wavs/ar-arabic.wav", + "./spoken-language-identification-test-wavs/bg-bulgarian.wav", + "./spoken-language-identification-test-wavs/de-german.wav", + ) + + for (waveFilename in testFiles) { + val objArray = WaveReader.readWaveFromFile( + filename = waveFilename, + ) + val samples: FloatArray = objArray[0] as FloatArray + val sampleRate: Int = objArray[1] as Int + + val stream = slid.createStream() + stream.acceptWaveform(samples, sampleRate = sampleRate) + val lang = slid.compute(stream) + stream.release() + println(waveFilename) + println(lang) + } +} + fun testAudioTagging() { val config = AudioTaggingConfig( model=AudioTaggingModelConfig( diff --git a/sherpa-onnx/jni/AudioTagging.kt b/kotlin-api-examples/SpokenLanguageIdentification.kt similarity index 53% rename from sherpa-onnx/jni/AudioTagging.kt rename to kotlin-api-examples/SpokenLanguageIdentification.kt index f3d827796..ef117c8bf 100644 --- a/sherpa-onnx/jni/AudioTagging.kt +++ b/kotlin-api-examples/SpokenLanguageIdentification.kt @@ -5,32 +5,22 @@ import android.util.Log private val TAG = "sherpa-onnx" -data class OfflineZipformerAudioTaggingModelConfig ( - val model: String, +data class SpokenLanguageIdentificationWhisperConfig ( + var encoder: String, + var decoder: String, + var tailPaddings: Int = -1, ) -data class AudioTaggingModelConfig ( - var zipformer: OfflineZipformerAudioTaggingModelConfig, +data class SpokenLanguageIdentificationConfig ( + var whisper: SpokenLanguageIdentificationWhisperConfig, var numThreads: Int = 1, var debug: Boolean = false, var provider: String = "cpu", ) -data class AudioTaggingConfig ( - var model: AudioTaggingModelConfig, - var labels: String, - var topK: Int = 5, -) - -data class AudioEvent ( - val name: String, - val index: Int, - val prob: Float, -) - -class AudioTagging( +class SpokenLanguageIdentification ( assetManager: AssetManager? = null, - config: AudioTaggingConfig, + config: SpokenLanguageIdentificationConfig, ) { private var ptr: Long @@ -43,10 +33,10 @@ class AudioTagging( } protected fun finalize() { - if(ptr != 0) { - delete(ptr) - ptr = 0 - } + if (ptr != 0L) { + delete(ptr) + ptr = 0 + } } fun release() = finalize() @@ -56,25 +46,22 @@ class AudioTagging( return OfflineStream(p) } - // fun compute(stream: OfflineStream, topK: Int=-1): Array { - fun compute(stream: OfflineStream, topK: Int=-1): Array { - var events :Array = compute(ptr, stream.ptr, topK) - } + fun compute(stream: OfflineStream) = compute(ptr, stream.ptr) private external fun newFromAsset( assetManager: AssetManager, - config: AudioTaggingConfig, + config: SpokenLanguageIdentificationConfig, ): Long private external fun newFromFile( - config: AudioTaggingConfig, + config: SpokenLanguageIdentificationConfig, ): Long private external fun delete(ptr: Long) private external fun createStream(ptr: Long): Long - private external fun compute(ptr: Long, streamPtr: Long, topK: Int): Array + private external fun compute(ptr: Long, streamPtr: Long): String companion object { init { diff --git a/kotlin-api-examples/run.sh b/kotlin-api-examples/run.sh index 750a70e5c..f14e169cd 100755 --- a/kotlin-api-examples/run.sh +++ b/kotlin-api-examples/run.sh @@ -30,19 +30,19 @@ cd ../kotlin-api-examples function testSpeakerEmbeddingExtractor() { if [ ! -f ./3dspeaker_speech_eres2net_large_sv_zh-cn_3dspeaker_16k.onnx ]; then - wget -q https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/3dspeaker_speech_eres2net_large_sv_zh-cn_3dspeaker_16k.onnx + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/3dspeaker_speech_eres2net_large_sv_zh-cn_3dspeaker_16k.onnx fi if [ ! -f ./speaker1_a_cn_16k.wav ]; then - wget -q https://github.com/csukuangfj/sr-data/raw/main/test/3d-speaker/speaker1_a_cn_16k.wav + curl -SL -O https://github.com/csukuangfj/sr-data/raw/main/test/3d-speaker/speaker1_a_cn_16k.wav fi if [ ! -f ./speaker1_b_cn_16k.wav ]; then - wget -q https://github.com/csukuangfj/sr-data/raw/main/test/3d-speaker/speaker1_b_cn_16k.wav + curl -SL -O https://github.com/csukuangfj/sr-data/raw/main/test/3d-speaker/speaker1_b_cn_16k.wav fi if [ ! -f ./speaker2_a_cn_16k.wav ]; then - wget -q https://github.com/csukuangfj/sr-data/raw/main/test/3d-speaker/speaker2_a_cn_16k.wav + curl -SL -O https://github.com/csukuangfj/sr-data/raw/main/test/3d-speaker/speaker2_a_cn_16k.wav fi } @@ -53,7 +53,7 @@ function testAsr() { fi if [ ! -d ./sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13 ]; then - wget -q https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13.tar.bz2 + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13.tar.bz2 tar xvf sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13.tar.bz2 rm sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13.tar.bz2 fi @@ -61,7 +61,7 @@ function testAsr() { function testTts() { if [ ! -f ./vits-piper-en_US-amy-low/en_US-amy-low.onnx ]; then - wget -q https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-piper-en_US-amy-low.tar.bz2 + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-piper-en_US-amy-low.tar.bz2 tar xf vits-piper-en_US-amy-low.tar.bz2 rm vits-piper-en_US-amy-low.tar.bz2 fi @@ -75,7 +75,22 @@ function testAudioTagging() { fi } +function testSpokenLanguageIdentification() { + if [ ! -f ./sherpa-onnx-whisper-tiny/tiny-encoder.int8.onnx ]; then + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-whisper-tiny.tar.bz2 + tar xvf sherpa-onnx-whisper-tiny.tar.bz2 + rm sherpa-onnx-whisper-tiny.tar.bz2 + fi + + if [ ! -f ./spoken-language-identification-test-wavs/ar-arabic.wav ]; then + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/spoken-language-identification-test-wavs.tar.bz2 + tar xvf spoken-language-identification-test-wavs.tar.bz2 + rm spoken-language-identification-test-wavs.tar.bz2 + fi +} + function test() { + testSpokenLanguageIdentification testAudioTagging testSpeakerEmbeddingExtractor testAsr @@ -90,6 +105,7 @@ kotlinc-jvm -include-runtime -d main.jar \ OfflineStream.kt \ SherpaOnnx.kt \ Speaker.kt \ + SpokenLanguageIdentification.kt \ Tts.kt \ WaveReader.kt \ faked-asset-manager.kt \ @@ -101,13 +117,13 @@ java -Djava.library.path=../build/lib -jar main.jar function testTwoPass() { if [ ! -f ./sherpa-onnx-streaming-zipformer-en-20M-2023-02-17/encoder-epoch-99-avg-1.int8.onnx ]; then - wget https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-en-20M-2023-02-17.tar.bz2 + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-en-20M-2023-02-17.tar.bz2 tar xvf sherpa-onnx-streaming-zipformer-en-20M-2023-02-17.tar.bz2 rm sherpa-onnx-streaming-zipformer-en-20M-2023-02-17.tar.bz2 fi if [ ! -f ./sherpa-onnx-whisper-tiny.en/tiny.en-encoder.int8.onnx ]; then - wget https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-whisper-tiny.en.tar.bz2 + curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-whisper-tiny.en.tar.bz2 tar xvf sherpa-onnx-whisper-tiny.en.tar.bz2 rm sherpa-onnx-whisper-tiny.en.tar.bz2 fi diff --git a/sherpa-onnx/jni/CMakeLists.txt b/sherpa-onnx/jni/CMakeLists.txt index 75b6a1bb5..6f14a35fa 100644 --- a/sherpa-onnx/jni/CMakeLists.txt +++ b/sherpa-onnx/jni/CMakeLists.txt @@ -13,6 +13,7 @@ add_library(sherpa-onnx-jni audio-tagging.cc jni.cc offline-stream.cc + spoken-language-identification.cc ) target_link_libraries(sherpa-onnx-jni sherpa-onnx-core) install(TARGETS sherpa-onnx-jni DESTINATION lib) diff --git a/sherpa-onnx/jni/spoken-language-identification.cc b/sherpa-onnx/jni/spoken-language-identification.cc new file mode 100644 index 000000000..0bff585d4 --- /dev/null +++ b/sherpa-onnx/jni/spoken-language-identification.cc @@ -0,0 +1,104 @@ +// sherpa-onnx/jni/spoken-language-identification.cc +// +// Copyright (c) 2024 Xiaomi Corporation + +#include "sherpa-onnx/csrc/spoken-language-identification.h" + +#include "sherpa-onnx/csrc/macros.h" +#include "sherpa-onnx/jni/common.h" + +namespace sherpa_onnx { + +static SpokenLanguageIdentificationConfig GetSpokenLanguageIdentificationConfig( + JNIEnv *env, jobject config) { + SpokenLanguageIdentificationConfig ans; + + jclass cls = env->GetObjectClass(config); + jfieldID fid = env->GetFieldID( + cls, "whisper", + "Lcom/k2fsa/sherpa/onnx/SpokenLanguageIdentificationWhisperConfig;"); + + jobject whisper = env->GetObjectField(config, fid); + jclass whisper_cls = env->GetObjectClass(whisper); + + fid = env->GetFieldID(whisper_cls, "encoder", "Ljava/lang/String;"); + + jstring s = (jstring)env->GetObjectField(whisper, fid); + const char *p = env->GetStringUTFChars(s, nullptr); + ans.whisper.encoder = p; + env->ReleaseStringUTFChars(s, p); + + fid = env->GetFieldID(whisper_cls, "decoder", "Ljava/lang/String;"); + s = (jstring)env->GetObjectField(whisper, fid); + p = env->GetStringUTFChars(s, nullptr); + ans.whisper.decoder = p; + env->ReleaseStringUTFChars(s, p); + + fid = env->GetFieldID(whisper_cls, "tailPaddings", "I"); + ans.whisper.tail_paddings = env->GetIntField(whisper, fid); + + fid = env->GetFieldID(cls, "numThreads", "I"); + ans.num_threads = env->GetIntField(config, fid); + + fid = env->GetFieldID(cls, "debug", "Z"); + ans.debug = env->GetBooleanField(config, fid); + + fid = env->GetFieldID(cls, "provider", "Ljava/lang/String;"); + s = (jstring)env->GetObjectField(config, fid); + p = env->GetStringUTFChars(s, nullptr); + ans.provider = p; + env->ReleaseStringUTFChars(s, p); + + return ans; +} + +} // namespace sherpa_onnx + +SHERPA_ONNX_EXTERN_C +JNIEXPORT jlong JNICALL +Java_com_k2fsa_sherpa_onnx_SpokenLanguageIdentification_newFromFile( + JNIEnv *env, jobject /*obj*/, jobject _config) { + auto config = + sherpa_onnx::GetSpokenLanguageIdentificationConfig(env, _config); + SHERPA_ONNX_LOGE("SpokenLanguageIdentification newFromFile config:\n%s", + config.ToString().c_str()); + + if (!config.Validate()) { + SHERPA_ONNX_LOGE("Errors found in config!"); + return 0; + } + + auto tagger = new sherpa_onnx::SpokenLanguageIdentification(config); + + return (jlong)tagger; +} + +SHERPA_ONNX_EXTERN_C +JNIEXPORT jlong JNICALL +Java_com_k2fsa_sherpa_onnx_SpokenLanguageIdentification_createStream( + JNIEnv *env, jobject /*obj*/, jlong ptr) { + auto slid = + reinterpret_cast(ptr); + std::unique_ptr s = slid->CreateStream(); + + // The user is responsible to free the returned pointer. + // + // See Java_com_k2fsa_sherpa_onnx_OfflineStream_delete() from + // ./offline-stream.cc + sherpa_onnx::OfflineStream *p = s.release(); + return (jlong)p; +} + +SHERPA_ONNX_EXTERN_C +JNIEXPORT jstring JNICALL +Java_com_k2fsa_sherpa_onnx_SpokenLanguageIdentification_compute(JNIEnv *env, + jobject /*obj*/, + jlong ptr, + jlong s_ptr) { + sherpa_onnx::SpokenLanguageIdentification *slid = + reinterpret_cast(ptr); + sherpa_onnx::OfflineStream *s = + reinterpret_cast(s_ptr); + std::string lang = slid->Compute(s); + return env->NewStringUTF(lang.c_str()); +} From d97a283dbb124b97f7560026f5db8490baa76f04 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Thu, 18 Apr 2024 14:33:59 +0800 Subject: [PATCH 34/45] Add Android demo for spoken language identification using Whisper multilingual models (#783) --- .../apk-spoken-language-identification.yaml | 174 ++++++++++++++++ .../.gitignore | 15 ++ .../app/.gitignore | 1 + .../app/build.gradle.kts | 69 +++++++ .../app/proguard-rules.pro | 21 ++ .../onnx/slid/ExampleInstrumentedTest.kt | 24 +++ .../app/src/main/AndroidManifest.xml | 30 +++ .../app/src/main/assets/.gitignore | 0 .../java/com/k2fsa/sherpa/onnx/slid/Home.kt | 171 ++++++++++++++++ .../k2fsa/sherpa/onnx/slid/MainActivity.kt | 74 +++++++ .../k2fsa/sherpa/onnx/slid/OfflineStream.kt | 1 + .../onnx/slid/SpokenLanguageIdentification.kt | 102 ++++++++++ .../java/com/k2fsa/sherpa/onnx/slid/slid.kt | 42 ++++ .../k2fsa/sherpa/onnx/slid/ui/theme/Color.kt | 11 ++ .../k2fsa/sherpa/onnx/slid/ui/theme/Theme.kt | 70 +++++++ .../k2fsa/sherpa/onnx/slid/ui/theme/Type.kt | 34 ++++ .../app/src/main/jniLibs/arm64-v8a/.gitignore | 0 .../src/main/jniLibs/armeabi-v7a/.gitignore | 0 .../app/src/main/jniLibs/x86/.gitignore | 0 .../app/src/main/jniLibs/x86_64/.gitignore | 0 .../drawable-v24/ic_launcher_foreground.xml | 30 +++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../app/src/main/res/values/colors.xml | 10 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/themes.xml | 5 + .../app/src/main/res/xml/backup_rules.xml | 13 ++ .../main/res/xml/data_extraction_rules.xml | 19 ++ .../k2fsa/sherpa/onnx/slid/ExampleUnitTest.kt | 17 ++ .../build.gradle.kts | 5 + .../gradle.properties | 23 +++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + .../gradlew | 185 ++++++++++++++++++ .../gradlew.bat | 89 +++++++++ .../settings.gradle.kts | 17 ++ .../SpokenLanguageIdentification.kt | 72 +------ scripts/apk/build-apk-slid.sh.in | 91 +++++++++ scripts/apk/generate-slid-apk-script.py | 90 +++++++++ sherpa-onnx/csrc/audio-tagging-impl.cc | 2 + sherpa-onnx/csrc/audio-tagging.cc | 2 + sherpa-onnx/csrc/offline-whisper-model.cc | 22 +++ sherpa-onnx/csrc/offline-whisper-model.h | 2 + .../spoken-language-identification-impl.cc | 35 ++++ .../spoken-language-identification-impl.h | 10 + ...ken-language-identification-whisper-impl.h | 14 ++ .../csrc/spoken-language-identification.cc | 11 ++ .../csrc/spoken-language-identification.h | 10 + .../jni/spoken-language-identification.cc | 34 ++++ 60 files changed, 1767 insertions(+), 71 deletions(-) create mode 100644 .github/workflows/apk-spoken-language-identification.yaml create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/.gitignore create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/.gitignore create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/build.gradle.kts create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/proguard-rules.pro create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/androidTest/java/com/k2fsa/sherpa/onnx/slid/ExampleInstrumentedTest.kt create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/AndroidManifest.xml create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/assets/.gitignore create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/Home.kt create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/MainActivity.kt create mode 120000 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/OfflineStream.kt create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/SpokenLanguageIdentification.kt create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/slid.kt create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/ui/theme/Color.kt create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/ui/theme/Theme.kt create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/ui/theme/Type.kt create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/jniLibs/arm64-v8a/.gitignore create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/jniLibs/armeabi-v7a/.gitignore create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/jniLibs/x86/.gitignore create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/jniLibs/x86_64/.gitignore create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/values/colors.xml create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/values/strings.xml create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/values/themes.xml create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/xml/backup_rules.xml create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/app/src/test/java/com/k2fsa/sherpa/onnx/slid/ExampleUnitTest.kt create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/build.gradle.kts create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/gradle.properties create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/gradle/wrapper/gradle-wrapper.properties create mode 100755 android/SherpaOnnxSpokenLanguageIdentification/gradlew create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/gradlew.bat create mode 100644 android/SherpaOnnxSpokenLanguageIdentification/settings.gradle.kts mode change 100644 => 120000 kotlin-api-examples/SpokenLanguageIdentification.kt create mode 100644 scripts/apk/build-apk-slid.sh.in create mode 100755 scripts/apk/generate-slid-apk-script.py diff --git a/.github/workflows/apk-spoken-language-identification.yaml b/.github/workflows/apk-spoken-language-identification.yaml new file mode 100644 index 000000000..39e1e1b7f --- /dev/null +++ b/.github/workflows/apk-spoken-language-identification.yaml @@ -0,0 +1,174 @@ +name: apk-slid + +on: + push: + tags: + - '*' + + workflow_dispatch: + +concurrency: + group: apk-slid-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + +jobs: + apk_slid: + if: github.repository_owner == 'csukuangfj' || github.repository_owner == 'k2-fsa' + runs-on: ${{ matrix.os }} + name: apk for slid ${{ matrix.index }}/${{ matrix.total }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + total: ["1"] + index: ["0"] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # https://github.com/actions/setup-java + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '21' + + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: ${{ matrix.os }}-android + + - name: Display NDK HOME + shell: bash + run: | + echo "ANDROID_NDK_LATEST_HOME: ${ANDROID_NDK_LATEST_HOME}" + ls -lh ${ANDROID_NDK_LATEST_HOME} + + - name: Install Python dependencies + shell: bash + run: | + python3 -m pip install --upgrade pip jinja2 + + - name: Setup build tool version variable + shell: bash + run: | + echo "---" + ls -lh /usr/local/lib/android/ + echo "---" + + ls -lh /usr/local/lib/android/sdk + echo "---" + + ls -lh /usr/local/lib/android/sdk/build-tools + echo "---" + + BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1) + echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV + echo "Last build tool version is: $BUILD_TOOL_VERSION" + + - name: Generate build script + shell: bash + run: | + cd scripts/apk + + total=${{ matrix.total }} + index=${{ matrix.index }} + + ./generate-slid-apk-script.py --total $total --index $index + + chmod +x build-apk-slid.sh + mv -v ./build-apk-slid.sh ../.. + + - name: build APK + shell: bash + run: | + export CMAKE_CXX_COMPILER_LAUNCHER=ccache + export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" + cmake --version + + export ANDROID_NDK=$ANDROID_NDK_LATEST_HOME + ./build-apk-slid.sh + + - name: Display APK + shell: bash + run: | + ls -lh ./apks/ + du -h -d1 . + + # https://github.com/marketplace/actions/sign-android-release + - uses: r0adkll/sign-android-release@v1 + name: Sign app APK + with: + releaseDirectory: ./apks + signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} + alias: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }} + keyStorePassword: ${{ secrets.ANDROID_SIGNING_KEY_STORE_PASSWORD }} + env: + BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }} + + - name: Display APK for slid after signing + shell: bash + run: | + ls -lh ./apks/ + du -h -d1 . + + - name: Rename APK for slid after signing + shell: bash + run: | + cd apks + rm -fv signingKey.jks + rm -fv *.apk.idsig + rm -fv *-aligned.apk + + all_apks=$(ls -1 *-signed.apk) + echo "----" + echo $all_apks + echo "----" + for apk in ${all_apks[@]}; do + n=$(echo $apk | sed -e s/-signed//) + mv -v $apk $n + done + + cd .. + + ls -lh ./apks/ + du -h -d1 . + + - name: Display APK after rename + shell: bash + run: | + ls -lh ./apks/ + du -h -d1 . + + - name: Publish to huggingface + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + uses: nick-fields/retry@v3 + with: + max_attempts: 20 + timeout_seconds: 200 + shell: bash + command: | + git config --global user.email "csukuangfj@gmail.com" + git config --global user.name "Fangjun Kuang" + + rm -rf huggingface + export GIT_LFS_SKIP_SMUDGE=1 + + git clone https://huggingface.co/csukuangfj/sherpa-onnx-apk huggingface + cd huggingface + git fetch + git pull + git merge -m "merge remote" --ff origin main + + mkdir -p slid + cp -v ../apks/*.apk ./slid/ + git status + git lfs track "*.apk" + git add . + git commit -m "add more apks" + git push https://csukuangfj:$HF_TOKEN@huggingface.co/csukuangfj/sherpa-onnx-apk main diff --git a/android/SherpaOnnxSpokenLanguageIdentification/.gitignore b/android/SherpaOnnxSpokenLanguageIdentification/.gitignore new file mode 100644 index 000000000..aa724b770 --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/.gitignore b/android/SherpaOnnxSpokenLanguageIdentification/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/build.gradle.kts b/android/SherpaOnnxSpokenLanguageIdentification/app/build.gradle.kts new file mode 100644 index 000000000..638582676 --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/build.gradle.kts @@ -0,0 +1,69 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.k2fsa.sherpa.onnx.slid" + compileSdk = 34 + + defaultConfig { + applicationId = "com.k2fsa.sherpa.onnx.slid" + minSdk = 21 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("androidx.activity:activity-compose:1.8.2") + implementation(platform("androidx.compose:compose-bom:2023.08.00")) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00")) + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") +} \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/proguard-rules.pro b/android/SherpaOnnxSpokenLanguageIdentification/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/androidTest/java/com/k2fsa/sherpa/onnx/slid/ExampleInstrumentedTest.kt b/android/SherpaOnnxSpokenLanguageIdentification/app/src/androidTest/java/com/k2fsa/sherpa/onnx/slid/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..5cb3e238d --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/androidTest/java/com/k2fsa/sherpa/onnx/slid/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.k2fsa.sherpa.onnx.slid + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.k2fsa.sherpa.onnx.slid", appContext.packageName) + } +} \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/AndroidManifest.xml b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..df44766e2 --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/assets/.gitignore b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/assets/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/Home.kt b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/Home.kt new file mode 100644 index 000000000..018e39134 --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/Home.kt @@ -0,0 +1,171 @@ +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) + +package com.k2fsa.sherpa.onnx.slid + +import android.Manifest +import android.app.Activity +import android.content.pm.PackageManager +import android.media.AudioFormat +import android.media.AudioRecord +import android.media.MediaRecorder +import android.util.Log +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.Modifier +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.app.ActivityCompat +import kotlin.concurrent.thread + +@Composable +fun Home() { + Scaffold( + topBar = { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text( + "Next-gen Kaldi: Spoken language identification", + fontWeight = FontWeight.Bold, + fontSize = 13.sp, + ) + }, + ) + }, + content = { + MyApp(it) + }, + ) +} + +private var audioRecord: AudioRecord? = null +private val sampleRateInHz = 16000 + +@Composable +fun MyApp(padding: PaddingValues) { + val activity = LocalContext.current as Activity + var isStarted by remember { mutableStateOf(false) } + var result by remember { mutableStateOf("") } + + val onButtonClick: () -> Unit = { + isStarted = !isStarted + if (isStarted) { + result = "" + if (ActivityCompat.checkSelfPermission( + activity, + Manifest.permission.RECORD_AUDIO + ) != PackageManager.PERMISSION_GRANTED + ) { + Log.i(TAG, "Recording is not allowed") + } else { + val audioSource = MediaRecorder.AudioSource.MIC + val channelConfig = AudioFormat.CHANNEL_IN_MONO + val audioFormat = AudioFormat.ENCODING_PCM_16BIT + val numBytes = + AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat) + + audioRecord = AudioRecord( + audioSource, + sampleRateInHz, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT, + numBytes * 2 // a sample has two bytes as we are using 16-bit PCM + ) + + thread(true) { + Log.i(TAG, "processing samples") + val interval = 0.1 // i.e., 100 ms + val bufferSize = (interval * sampleRateInHz).toInt() // in samples + val buffer = ShortArray(bufferSize) + val sampleList = ArrayList() + audioRecord?.let { + it.startRecording() + while (isStarted) { + val ret = it.read(buffer, 0, buffer.size) + ret.let { n -> + val samples = FloatArray(n) { buffer[it] / 32768.0f } + sampleList.add(samples) + } + } + } + Log.i(TAG, "Stop recording") + Log.i(TAG, "Start recognition") + val samples = Flatten(sampleList) + val stream = Slid.slid.createStream() + stream.acceptWaveform(samples, sampleRateInHz) + val lang = Slid.slid.compute(stream) + + result = Slid.localeMap.get(lang) ?: lang + + stream.release() + } + } + } + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter + ) { + Column( + Modifier.padding(padding), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = onButtonClick) { + if (isStarted) { + Text("Stop") + } else { + Text("Start") + } + } + + Spacer(modifier = Modifier.height(16.dp)) + if (result.isNotEmpty() && result.isNotBlank()) { + Text("Detected language: $result") + } + } + } +} + +fun Flatten(sampleList: ArrayList): FloatArray { + var totalSamples = 0 + for (a in sampleList) { + totalSamples += a.size + } + var i = 0 + val samples = FloatArray(totalSamples) + for (a in sampleList) { + for (s in a) { + samples[i] = s + i += 1 + } + } + Log.i(TAG, "$i, $totalSamples") + + return samples +} \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/MainActivity.kt b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/MainActivity.kt new file mode 100644 index 000000000..dfbcba160 --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/MainActivity.kt @@ -0,0 +1,74 @@ +package com.k2fsa.sherpa.onnx.slid + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.core.app.ActivityCompat +import com.k2fsa.sherpa.onnx.SpokenLanguageIdentification +import com.k2fsa.sherpa.onnx.slid.ui.theme.SherpaOnnxSpokenLanguageIdentificationTheme + +const val TAG = "sherpa-onnx" +private const val REQUEST_RECORD_AUDIO_PERMISSION = 200 + +class MainActivity : ComponentActivity() { + private val permissions: Array = arrayOf(Manifest.permission.RECORD_AUDIO) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + SpokenLanguageIdentificationApp() + } + ActivityCompat.requestPermissions(this, permissions, REQUEST_RECORD_AUDIO_PERMISSION) + Slid.initSlid(this.assets) + } + @Suppress("DEPRECATION") + @Deprecated("Deprecated in Java") + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + val permissionToRecordAccepted = if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION) { + grantResults[0] == PackageManager.PERMISSION_GRANTED + } else { + false + } + + if (!permissionToRecordAccepted) { + Log.e(TAG, "Audio record is disallowed") + Toast.makeText( + this, + "This App needs access to the microphone", + Toast.LENGTH_SHORT + ) + .show() + finish() + } + Log.i(TAG, "Audio record is permitted") + } +} + +@Composable +fun SpokenLanguageIdentificationApp() { + SherpaOnnxSpokenLanguageIdentificationTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Home() + } + } +} \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/OfflineStream.kt b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/OfflineStream.kt new file mode 120000 index 000000000..1a5dfc316 --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/OfflineStream.kt @@ -0,0 +1 @@ +../../../../../../../../../../SherpaOnnxAudioTagging/app/src/main/java/com/k2fsa/sherpa/onnx/audio/tagging/OfflineStream.kt \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/SpokenLanguageIdentification.kt b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/SpokenLanguageIdentification.kt new file mode 100644 index 000000000..fedf9d65b --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/SpokenLanguageIdentification.kt @@ -0,0 +1,102 @@ +package com.k2fsa.sherpa.onnx + +import android.content.res.AssetManager +import android.util.Log + +private val TAG = "sherpa-onnx" + +data class SpokenLanguageIdentificationWhisperConfig ( + var encoder: String, + var decoder: String, + var tailPaddings: Int = -1, +) + +data class SpokenLanguageIdentificationConfig ( + var whisper: SpokenLanguageIdentificationWhisperConfig, + var numThreads: Int = 1, + var debug: Boolean = false, + var provider: String = "cpu", +) + +class SpokenLanguageIdentification ( + assetManager: AssetManager? = null, + config: SpokenLanguageIdentificationConfig, +) { + private var ptr: Long + + init { + ptr = if (assetManager != null) { + newFromAsset(assetManager, config) + } else { + newFromFile(config) + } + } + + protected fun finalize() { + if (ptr != 0L) { + delete(ptr) + ptr = 0 + } + } + + fun release() = finalize() + + fun createStream(): OfflineStream { + val p = createStream(ptr) + return OfflineStream(p) + } + + fun compute(stream: OfflineStream) = compute(ptr, stream.ptr) + + private external fun newFromAsset( + assetManager: AssetManager, + config: SpokenLanguageIdentificationConfig, + ): Long + + private external fun newFromFile( + config: SpokenLanguageIdentificationConfig, + ): Long + + private external fun delete(ptr: Long) + + private external fun createStream(ptr: Long): Long + + private external fun compute(ptr: Long, streamPtr: Long): String + + companion object { + init { + System.loadLibrary("sherpa-onnx-jni") + } + } +} +// please refer to +// https://k2-fsa.github.io/sherpa/onnx/spolken-language-identification/pretrained_models.html#whisper +// to download more models +fun getSpokenLanguageIdentificationConfig(type: Int, numThreads: Int=1): SpokenLanguageIdentificationConfig? { + when (type) { + 0 -> { + val modelDir = "sherpa-onnx-whisper-tiny" + return SpokenLanguageIdentificationConfig( + whisper = SpokenLanguageIdentificationWhisperConfig( + encoder = "$modelDir/tiny-encoder.int8.onnx", + decoder = "$modelDir/tiny-decoder.int8.onnx", + ), + numThreads = numThreads, + debug = true, + ) + } + + 1 -> { + val modelDir = "sherpa-onnx-whisper-base" + return SpokenLanguageIdentificationConfig( + whisper = SpokenLanguageIdentificationWhisperConfig( + encoder = "$modelDir/tiny-encoder.int8.onnx", + decoder = "$modelDir/tiny-decoder.int8.onnx", + ), + numThreads = 1, + debug = true, + ) + } + } + return null +} diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/slid.kt b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/slid.kt new file mode 100644 index 000000000..60c511704 --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/slid.kt @@ -0,0 +1,42 @@ +package com.k2fsa.sherpa.onnx.slid + +import android.content.res.AssetManager +import android.util.Log +import com.k2fsa.sherpa.onnx.SpokenLanguageIdentification +import com.k2fsa.sherpa.onnx.getSpokenLanguageIdentificationConfig +import java.util.Locale + + +object Slid { + private var _slid: SpokenLanguageIdentification? = null + + private var _localeMap = mutableMapOf() + val slid: SpokenLanguageIdentification + get() { + return _slid!! + } + val localeMap : Map + get() { + return _localeMap + } + + fun initSlid(assetManager: AssetManager? = null, numThreads: Int = 1) { + synchronized(this) { + if (_slid == null) { + + Log.i(TAG, "Initializing slid") + val config = + getSpokenLanguageIdentificationConfig(type = 0, numThreads = numThreads)!! + _slid = SpokenLanguageIdentification(assetManager, config) + } + + if (_localeMap.isEmpty()) { + val allLang = Locale.getISOLanguages(); + for (lang in allLang) { + val locale = Locale(lang) + _localeMap[lang] = locale.displayName + } + } + } + } +} \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/ui/theme/Color.kt b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/ui/theme/Color.kt new file mode 100644 index 000000000..cbfdfd17c --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.k2fsa.sherpa.onnx.slid.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/ui/theme/Theme.kt b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/ui/theme/Theme.kt new file mode 100644 index 000000000..02f83371a --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/ui/theme/Theme.kt @@ -0,0 +1,70 @@ +package com.k2fsa.sherpa.onnx.slid.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun SherpaOnnxSpokenLanguageIdentificationTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/ui/theme/Type.kt b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/ui/theme/Type.kt new file mode 100644 index 000000000..48bb5ae96 --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/java/com/k2fsa/sherpa/onnx/slid/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.k2fsa.sherpa.onnx.slid.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/jniLibs/arm64-v8a/.gitignore b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/jniLibs/arm64-v8a/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/jniLibs/armeabi-v7a/.gitignore b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/jniLibs/armeabi-v7a/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/jniLibs/x86/.gitignore b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/jniLibs/x86/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/jniLibs/x86_64/.gitignore b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/jniLibs/x86_64/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/drawable/ic_launcher_background.xml b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9 GIT binary patch literal 2898 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 literal 0 HcmV?d00001 diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i literal 0 HcmV?d00001 diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f GIT binary patch literal 3918 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�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s literal 0 HcmV?d00001 diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/values/colors.xml b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..f8c6127d3 --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/values/strings.xml b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..f0a3e3a4f --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + SherpaOnnxSpokenLanguageIdentification + \ No newline at end of file diff --git a/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/values/themes.xml b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..07b6588b3 --- /dev/null +++ b/android/SherpaOnnxSpokenLanguageIdentification/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +