mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2024-11-24 12:31:02 +00:00
aws: add wrapper for the polly text to speech API
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1899>
This commit is contained in:
parent
ce7d314349
commit
5f8e8b4873
9 changed files with 1821 additions and 10 deletions
25
Cargo.lock
generated
25
Cargo.lock
generated
|
@ -418,6 +418,29 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-polly"
|
||||
version = "1.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6985182fbfde1ba022b4c3bd0c13fe28dd5cbaec6c2b326836c0c376afc0f373"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-sigv4",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"bytes",
|
||||
"http 0.2.12",
|
||||
"once_cell",
|
||||
"regex-lite",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-s3"
|
||||
version = "1.59.0"
|
||||
|
@ -2387,9 +2410,11 @@ dependencies = [
|
|||
name = "gst-plugin-aws"
|
||||
version = "0.14.0-alpha.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
"aws-config",
|
||||
"aws-credential-types",
|
||||
"aws-sdk-polly",
|
||||
"aws-sdk-s3",
|
||||
"aws-sdk-transcribestreaming",
|
||||
"aws-sdk-translate",
|
||||
|
|
|
@ -2,6 +2,130 @@
|
|||
"aws": {
|
||||
"description": "GStreamer Amazon Web Services plugin",
|
||||
"elements": {
|
||||
"awspolly": {
|
||||
"author": "Mathieu Duponchelle <mathieu@centricular.com>",
|
||||
"description": "Text to Speech filter, using AWS polly",
|
||||
"hierarchy": [
|
||||
"GstAwsPolly",
|
||||
"GstElement",
|
||||
"GstObject",
|
||||
"GInitiallyUnowned",
|
||||
"GObject"
|
||||
],
|
||||
"klass": "Audio/Text/Filter",
|
||||
"pad-templates": {
|
||||
"sink": {
|
||||
"caps": "text/x-raw:\n format: utf8\napplication/ssml+xml:\n",
|
||||
"direction": "sink",
|
||||
"presence": "always"
|
||||
},
|
||||
"src": {
|
||||
"caps": "audio/x-raw:\n rate: 16000\n channels: 1\n layout: interleaved\n format: S16LE\n",
|
||||
"direction": "src",
|
||||
"presence": "always"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"access-key": {
|
||||
"blurb": "AWS Access Key",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "NULL",
|
||||
"mutable": "ready",
|
||||
"readable": true,
|
||||
"type": "gchararray",
|
||||
"writable": true
|
||||
},
|
||||
"engine": {
|
||||
"blurb": "Defines what engine to use",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "neural (1)",
|
||||
"mutable": "ready",
|
||||
"readable": true,
|
||||
"type": "GstAwsPollyEngine",
|
||||
"writable": true
|
||||
},
|
||||
"language-code": {
|
||||
"blurb": "Defines what language code to use",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "none (0)",
|
||||
"mutable": "ready",
|
||||
"readable": true,
|
||||
"type": "GstAwsPollyLanguageCode",
|
||||
"writable": true
|
||||
},
|
||||
"latency": {
|
||||
"blurb": "Amount of milliseconds to allow AWS Polly",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "2000",
|
||||
"max": "-1",
|
||||
"min": "0",
|
||||
"mutable": "ready",
|
||||
"readable": true,
|
||||
"type": "guint",
|
||||
"writable": true
|
||||
},
|
||||
"lexicon-names": {
|
||||
"blurb": "List of lexicon names to use",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"mutable": "ready",
|
||||
"readable": true,
|
||||
"type": "GstValueArray",
|
||||
"writable": true
|
||||
},
|
||||
"secret-access-key": {
|
||||
"blurb": "AWS Secret Access Key",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "NULL",
|
||||
"mutable": "ready",
|
||||
"readable": true,
|
||||
"type": "gchararray",
|
||||
"writable": true
|
||||
},
|
||||
"session-token": {
|
||||
"blurb": "AWS temporary Session Token from STS",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "NULL",
|
||||
"mutable": "ready",
|
||||
"readable": true,
|
||||
"type": "gchararray",
|
||||
"writable": true
|
||||
},
|
||||
"voice-id": {
|
||||
"blurb": "Defines what voice id to use",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "aria (2)",
|
||||
"mutable": "ready",
|
||||
"readable": true,
|
||||
"type": "GstAwsPollyVoiceId",
|
||||
"writable": true
|
||||
}
|
||||
},
|
||||
"rank": "none"
|
||||
},
|
||||
"awss3hlssink": {
|
||||
"author": "Daily. Co",
|
||||
"description": "Streams HLS data to S3",
|
||||
|
@ -1226,6 +1350,571 @@
|
|||
"filename": "gstaws",
|
||||
"license": "MPL",
|
||||
"other-types": {
|
||||
"GstAwsPollyEngine": {
|
||||
"kind": "enum",
|
||||
"values": [
|
||||
{
|
||||
"desc": "Standard",
|
||||
"name": "standard",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"desc": "Neural",
|
||||
"name": "neural",
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"GstAwsPollyLanguageCode": {
|
||||
"kind": "enum",
|
||||
"values": [
|
||||
{
|
||||
"desc": "None",
|
||||
"name": "none",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"desc": "Arb",
|
||||
"name": "arb",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"desc": "CaEs",
|
||||
"name": "ca-ES",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"desc": "CmnCn",
|
||||
"name": "cmn-CN",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"desc": "CyGb",
|
||||
"name": "cy-GB",
|
||||
"value": "4"
|
||||
},
|
||||
{
|
||||
"desc": "DaDk",
|
||||
"name": "da-DK",
|
||||
"value": "5"
|
||||
},
|
||||
{
|
||||
"desc": "DeAt",
|
||||
"name": "de-AT",
|
||||
"value": "6"
|
||||
},
|
||||
{
|
||||
"desc": "DeDe",
|
||||
"name": "de-DE",
|
||||
"value": "7"
|
||||
},
|
||||
{
|
||||
"desc": "EnAu",
|
||||
"name": "en-AU",
|
||||
"value": "8"
|
||||
},
|
||||
{
|
||||
"desc": "EnGb",
|
||||
"name": "en-GB",
|
||||
"value": "9"
|
||||
},
|
||||
{
|
||||
"desc": "EnGbWls",
|
||||
"name": "en-GB-WLS",
|
||||
"value": "10"
|
||||
},
|
||||
{
|
||||
"desc": "EnIn",
|
||||
"name": "en-IN",
|
||||
"value": "11"
|
||||
},
|
||||
{
|
||||
"desc": "EnNz",
|
||||
"name": "en-NZ",
|
||||
"value": "12"
|
||||
},
|
||||
{
|
||||
"desc": "EnUs",
|
||||
"name": "en-US",
|
||||
"value": "13"
|
||||
},
|
||||
{
|
||||
"desc": "EnZa",
|
||||
"name": "en-ZA",
|
||||
"value": "14"
|
||||
},
|
||||
{
|
||||
"desc": "EsEs",
|
||||
"name": "es-ES",
|
||||
"value": "15"
|
||||
},
|
||||
{
|
||||
"desc": "EsMx",
|
||||
"name": "es-MX",
|
||||
"value": "16"
|
||||
},
|
||||
{
|
||||
"desc": "EsUs",
|
||||
"name": "es-US",
|
||||
"value": "17"
|
||||
},
|
||||
{
|
||||
"desc": "FrCa",
|
||||
"name": "fr-CA",
|
||||
"value": "18"
|
||||
},
|
||||
{
|
||||
"desc": "FrFr",
|
||||
"name": "fr-FR",
|
||||
"value": "19"
|
||||
},
|
||||
{
|
||||
"desc": "HiIn",
|
||||
"name": "hi-IN",
|
||||
"value": "20"
|
||||
},
|
||||
{
|
||||
"desc": "IsIs",
|
||||
"name": "is-IS",
|
||||
"value": "21"
|
||||
},
|
||||
{
|
||||
"desc": "ItIt",
|
||||
"name": "it-IT",
|
||||
"value": "22"
|
||||
},
|
||||
{
|
||||
"desc": "JaJp",
|
||||
"name": "ja-JP",
|
||||
"value": "23"
|
||||
},
|
||||
{
|
||||
"desc": "KoKr",
|
||||
"name": "ko-KR",
|
||||
"value": "24"
|
||||
},
|
||||
{
|
||||
"desc": "NbNo",
|
||||
"name": "nb-NO",
|
||||
"value": "25"
|
||||
},
|
||||
{
|
||||
"desc": "NlNl",
|
||||
"name": "nl-NL",
|
||||
"value": "26"
|
||||
},
|
||||
{
|
||||
"desc": "PlPl",
|
||||
"name": "pl-PL",
|
||||
"value": "27"
|
||||
},
|
||||
{
|
||||
"desc": "PtBr",
|
||||
"name": "pt-BR",
|
||||
"value": "28"
|
||||
},
|
||||
{
|
||||
"desc": "PtPt",
|
||||
"name": "pt-PT",
|
||||
"value": "29"
|
||||
},
|
||||
{
|
||||
"desc": "RoRo",
|
||||
"name": "ro-RO",
|
||||
"value": "30"
|
||||
},
|
||||
{
|
||||
"desc": "RuRu",
|
||||
"name": "ru-RU",
|
||||
"value": "31"
|
||||
},
|
||||
{
|
||||
"desc": "SvSe",
|
||||
"name": "sv-SE",
|
||||
"value": "32"
|
||||
},
|
||||
{
|
||||
"desc": "TrTr",
|
||||
"name": "tr-TR",
|
||||
"value": "33"
|
||||
},
|
||||
{
|
||||
"desc": "YueCn",
|
||||
"name": "yue-CN",
|
||||
"value": "34"
|
||||
}
|
||||
]
|
||||
},
|
||||
"GstAwsPollyVoiceId": {
|
||||
"kind": "enum",
|
||||
"values": [
|
||||
{
|
||||
"desc": "Aditi",
|
||||
"name": "aditi",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"desc": "Amy",
|
||||
"name": "amy",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"desc": "Aria",
|
||||
"name": "aria",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"desc": "Arlet",
|
||||
"name": "arlet",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"desc": "Arthur",
|
||||
"name": "arthur",
|
||||
"value": "4"
|
||||
},
|
||||
{
|
||||
"desc": "Astrid",
|
||||
"name": "astrid",
|
||||
"value": "5"
|
||||
},
|
||||
{
|
||||
"desc": "Ayanda",
|
||||
"name": "ayanda",
|
||||
"value": "6"
|
||||
},
|
||||
{
|
||||
"desc": "Bianca",
|
||||
"name": "bianca",
|
||||
"value": "7"
|
||||
},
|
||||
{
|
||||
"desc": "Brian",
|
||||
"name": "brian",
|
||||
"value": "8"
|
||||
},
|
||||
{
|
||||
"desc": "Camila",
|
||||
"name": "camila",
|
||||
"value": "9"
|
||||
},
|
||||
{
|
||||
"desc": "Carla",
|
||||
"name": "carla",
|
||||
"value": "10"
|
||||
},
|
||||
{
|
||||
"desc": "Carmen",
|
||||
"name": "carmen",
|
||||
"value": "11"
|
||||
},
|
||||
{
|
||||
"desc": "Celine",
|
||||
"name": "celine",
|
||||
"value": "12"
|
||||
},
|
||||
{
|
||||
"desc": "Chantal",
|
||||
"name": "chantal",
|
||||
"value": "13"
|
||||
},
|
||||
{
|
||||
"desc": "Conchita",
|
||||
"name": "conchita",
|
||||
"value": "14"
|
||||
},
|
||||
{
|
||||
"desc": "Cristiano",
|
||||
"name": "cristiano",
|
||||
"value": "15"
|
||||
},
|
||||
{
|
||||
"desc": "Daniel",
|
||||
"name": "daniel",
|
||||
"value": "16"
|
||||
},
|
||||
{
|
||||
"desc": "Dora",
|
||||
"name": "dora",
|
||||
"value": "17"
|
||||
},
|
||||
{
|
||||
"desc": "Emma",
|
||||
"name": "emma",
|
||||
"value": "18"
|
||||
},
|
||||
{
|
||||
"desc": "Enrique",
|
||||
"name": "enrique",
|
||||
"value": "19"
|
||||
},
|
||||
{
|
||||
"desc": "Ewa",
|
||||
"name": "ewa",
|
||||
"value": "20"
|
||||
},
|
||||
{
|
||||
"desc": "Filiz",
|
||||
"name": "filiz",
|
||||
"value": "21"
|
||||
},
|
||||
{
|
||||
"desc": "Gabrielle",
|
||||
"name": "gabrielle",
|
||||
"value": "22"
|
||||
},
|
||||
{
|
||||
"desc": "Geraint",
|
||||
"name": "geraint",
|
||||
"value": "23"
|
||||
},
|
||||
{
|
||||
"desc": "Giorgio",
|
||||
"name": "giorgio",
|
||||
"value": "24"
|
||||
},
|
||||
{
|
||||
"desc": "Gwyneth",
|
||||
"name": "gwyneth",
|
||||
"value": "25"
|
||||
},
|
||||
{
|
||||
"desc": "Hannah",
|
||||
"name": "hannah",
|
||||
"value": "26"
|
||||
},
|
||||
{
|
||||
"desc": "Hans",
|
||||
"name": "hans",
|
||||
"value": "27"
|
||||
},
|
||||
{
|
||||
"desc": "Hiujin",
|
||||
"name": "hiujin",
|
||||
"value": "28"
|
||||
},
|
||||
{
|
||||
"desc": "Ines",
|
||||
"name": "ines",
|
||||
"value": "29"
|
||||
},
|
||||
{
|
||||
"desc": "Ivy",
|
||||
"name": "ivy",
|
||||
"value": "30"
|
||||
},
|
||||
{
|
||||
"desc": "Jacek",
|
||||
"name": "jacek",
|
||||
"value": "31"
|
||||
},
|
||||
{
|
||||
"desc": "Jan",
|
||||
"name": "jan",
|
||||
"value": "32"
|
||||
},
|
||||
{
|
||||
"desc": "Joanna",
|
||||
"name": "joanna",
|
||||
"value": "33"
|
||||
},
|
||||
{
|
||||
"desc": "Joey",
|
||||
"name": "joey",
|
||||
"value": "34"
|
||||
},
|
||||
{
|
||||
"desc": "Justin",
|
||||
"name": "justin",
|
||||
"value": "35"
|
||||
},
|
||||
{
|
||||
"desc": "Kajal",
|
||||
"name": "kajal",
|
||||
"value": "36"
|
||||
},
|
||||
{
|
||||
"desc": "Karl",
|
||||
"name": "karl",
|
||||
"value": "37"
|
||||
},
|
||||
{
|
||||
"desc": "Kendra",
|
||||
"name": "kendra",
|
||||
"value": "38"
|
||||
},
|
||||
{
|
||||
"desc": "Kevin",
|
||||
"name": "kevin",
|
||||
"value": "39"
|
||||
},
|
||||
{
|
||||
"desc": "Kimberly",
|
||||
"name": "kimberly",
|
||||
"value": "40"
|
||||
},
|
||||
{
|
||||
"desc": "Lea",
|
||||
"name": "lea",
|
||||
"value": "41"
|
||||
},
|
||||
{
|
||||
"desc": "Liam",
|
||||
"name": "liam",
|
||||
"value": "42"
|
||||
},
|
||||
{
|
||||
"desc": "Liv",
|
||||
"name": "liv",
|
||||
"value": "43"
|
||||
},
|
||||
{
|
||||
"desc": "Lotte",
|
||||
"name": "lotte",
|
||||
"value": "44"
|
||||
},
|
||||
{
|
||||
"desc": "Lucia",
|
||||
"name": "lucia",
|
||||
"value": "45"
|
||||
},
|
||||
{
|
||||
"desc": "Lupe",
|
||||
"name": "lupe",
|
||||
"value": "46"
|
||||
},
|
||||
{
|
||||
"desc": "Mads",
|
||||
"name": "mads",
|
||||
"value": "47"
|
||||
},
|
||||
{
|
||||
"desc": "Maja",
|
||||
"name": "maja",
|
||||
"value": "48"
|
||||
},
|
||||
{
|
||||
"desc": "Marlene",
|
||||
"name": "marlene",
|
||||
"value": "49"
|
||||
},
|
||||
{
|
||||
"desc": "Mathieu",
|
||||
"name": "mathieu",
|
||||
"value": "50"
|
||||
},
|
||||
{
|
||||
"desc": "Matthew",
|
||||
"name": "matthew",
|
||||
"value": "51"
|
||||
},
|
||||
{
|
||||
"desc": "Maxim",
|
||||
"name": "maxim",
|
||||
"value": "52"
|
||||
},
|
||||
{
|
||||
"desc": "Mia",
|
||||
"name": "mia",
|
||||
"value": "53"
|
||||
},
|
||||
{
|
||||
"desc": "Miguel",
|
||||
"name": "miguel",
|
||||
"value": "54"
|
||||
},
|
||||
{
|
||||
"desc": "Mizuki",
|
||||
"name": "mizuki",
|
||||
"value": "55"
|
||||
},
|
||||
{
|
||||
"desc": "Naja",
|
||||
"name": "naja",
|
||||
"value": "56"
|
||||
},
|
||||
{
|
||||
"desc": "Nicole",
|
||||
"name": "nicole",
|
||||
"value": "57"
|
||||
},
|
||||
{
|
||||
"desc": "Olivia",
|
||||
"name": "olivia",
|
||||
"value": "58"
|
||||
},
|
||||
{
|
||||
"desc": "Pedro",
|
||||
"name": "pedro",
|
||||
"value": "59"
|
||||
},
|
||||
{
|
||||
"desc": "Penelope",
|
||||
"name": "penelope",
|
||||
"value": "60"
|
||||
},
|
||||
{
|
||||
"desc": "Raveena",
|
||||
"name": "raveena",
|
||||
"value": "61"
|
||||
},
|
||||
{
|
||||
"desc": "Ricardo",
|
||||
"name": "ricardo",
|
||||
"value": "62"
|
||||
},
|
||||
{
|
||||
"desc": "Ruben",
|
||||
"name": "ruben",
|
||||
"value": "63"
|
||||
},
|
||||
{
|
||||
"desc": "Russell",
|
||||
"name": "russell",
|
||||
"value": "64"
|
||||
},
|
||||
{
|
||||
"desc": "Salli",
|
||||
"name": "salli",
|
||||
"value": "65"
|
||||
},
|
||||
{
|
||||
"desc": "Seoyeon",
|
||||
"name": "seoyeon",
|
||||
"value": "66"
|
||||
},
|
||||
{
|
||||
"desc": "Takumi",
|
||||
"name": "takumi",
|
||||
"value": "67"
|
||||
},
|
||||
{
|
||||
"desc": "Tatyana",
|
||||
"name": "tatyana",
|
||||
"value": "68"
|
||||
},
|
||||
{
|
||||
"desc": "Vicki",
|
||||
"name": "vicki",
|
||||
"value": "69"
|
||||
},
|
||||
{
|
||||
"desc": "Vitoria",
|
||||
"name": "vitoria",
|
||||
"value": "70"
|
||||
},
|
||||
{
|
||||
"desc": "Zeina",
|
||||
"name": "zeina",
|
||||
"value": "71"
|
||||
},
|
||||
{
|
||||
"desc": "Zhiyu",
|
||||
"name": "zhiyu",
|
||||
"value": "72"
|
||||
}
|
||||
]
|
||||
},
|
||||
"GstAwsTranscriberResultStability": {
|
||||
"kind": "enum",
|
||||
"values": [
|
||||
|
|
|
@ -19,6 +19,7 @@ aws-sdk-transcribestreaming = "1.0"
|
|||
aws-sdk-translate = "1.0"
|
||||
aws-types = "1.0"
|
||||
aws-credential-types = "1.0"
|
||||
aws-sdk-polly = "1.0"
|
||||
bytes = "1.0"
|
||||
futures = "0.3"
|
||||
gio.workspace = true
|
||||
|
@ -33,6 +34,7 @@ serde_json = "1"
|
|||
url = "2"
|
||||
gst-video = { workspace = true, features = ["v1_22"] }
|
||||
sprintf = "0.2"
|
||||
anyhow = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
chrono = { version = "0.4", features = [ "alloc" ] }
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
*/
|
||||
use gst::glib;
|
||||
|
||||
mod polly;
|
||||
mod s3hlssink;
|
||||
mod s3sink;
|
||||
mod s3src;
|
||||
|
@ -30,6 +31,7 @@ fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
|||
transcribe_parse::register(plugin)?;
|
||||
transcriber::register(plugin)?;
|
||||
s3hlssink::register(plugin)?;
|
||||
polly::register(plugin)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
677
net/aws/src/polly/imp.rs
Normal file
677
net/aws/src/polly/imp.rs
Normal file
|
@ -0,0 +1,677 @@
|
|||
// Copyright (C) 2024 Mathieu Duponchelle <mathieu@centricular.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//! AWS Polly element.
|
||||
//!
|
||||
//! This element calls AWS Polly to generate audio speech from text.
|
||||
|
||||
use gst::subclass::prelude::*;
|
||||
use gst::{glib, prelude::*};
|
||||
|
||||
use aws_sdk_s3::config::StalledStreamProtectionConfig;
|
||||
|
||||
use futures::future::{abortable, AbortHandle};
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use super::{AwsPollyEngine, AwsPollyLanguageCode, AwsPollyVoiceId, CAT};
|
||||
use crate::s3utils::RUNTIME;
|
||||
use anyhow::{anyhow, Error};
|
||||
|
||||
#[allow(deprecated)]
|
||||
static AWS_BEHAVIOR_VERSION: LazyLock<aws_config::BehaviorVersion> =
|
||||
LazyLock::new(aws_config::BehaviorVersion::v2023_11_09);
|
||||
|
||||
const DEFAULT_REGION: &str = "us-east-1";
|
||||
const DEFAULT_LATENCY: gst::ClockTime = gst::ClockTime::from_seconds(2);
|
||||
const DEFAULT_ENGINE: AwsPollyEngine = AwsPollyEngine::Neural;
|
||||
const DEFAULT_LANGUAGE_CODE: AwsPollyLanguageCode = AwsPollyLanguageCode::None;
|
||||
const DEFAULT_VOICE_ID: AwsPollyVoiceId = AwsPollyVoiceId::Aria;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct Settings {
|
||||
latency: gst::ClockTime,
|
||||
access_key: Option<String>,
|
||||
secret_access_key: Option<String>,
|
||||
session_token: Option<String>,
|
||||
engine: AwsPollyEngine,
|
||||
language_code: AwsPollyLanguageCode,
|
||||
voice_id: AwsPollyVoiceId,
|
||||
lexicon_names: gst::Array,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
latency: DEFAULT_LATENCY,
|
||||
access_key: None,
|
||||
secret_access_key: None,
|
||||
session_token: None,
|
||||
engine: DEFAULT_ENGINE,
|
||||
language_code: DEFAULT_LANGUAGE_CODE,
|
||||
voice_id: DEFAULT_VOICE_ID,
|
||||
lexicon_names: gst::Array::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
out_segment: gst::FormattedSegment<gst::ClockTime>,
|
||||
client: Option<aws_sdk_polly::Client>,
|
||||
send_abort_handle: Option<AbortHandle>,
|
||||
in_format: Option<aws_sdk_polly::types::TextType>,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
out_segment: gst::FormattedSegment::new(),
|
||||
client: None,
|
||||
send_abort_handle: None,
|
||||
in_format: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Polly {
|
||||
srcpad: gst::Pad,
|
||||
sinkpad: gst::Pad,
|
||||
settings: Mutex<Settings>,
|
||||
state: Mutex<State>,
|
||||
pub(super) aws_config: Mutex<Option<aws_config::SdkConfig>>,
|
||||
}
|
||||
|
||||
impl Polly {
|
||||
fn sink_event(&self, pad: &gst::Pad, event: gst::Event) -> bool {
|
||||
gst::log!(CAT, obj = pad, "Handling event {event:?}");
|
||||
|
||||
use gst::EventView::*;
|
||||
match event.view() {
|
||||
FlushStart(_) => {
|
||||
gst::info!(CAT, imp = self, "Received flush start, disconnecting");
|
||||
let ret = gst::Pad::event_default(pad, Some(&*self.obj()), event);
|
||||
self.disconnect();
|
||||
ret
|
||||
}
|
||||
Segment(e) => {
|
||||
let segment = match e.segment().clone().downcast::<gst::ClockTime>() {
|
||||
Err(segment) => {
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::StreamError::Format,
|
||||
["Only Time segments supported, got {:?}", segment.format(),]
|
||||
);
|
||||
return false;
|
||||
}
|
||||
Ok(segment) => segment,
|
||||
};
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.out_segment = segment;
|
||||
gst::Pad::event_default(pad, Some(&*self.obj()), event)
|
||||
}
|
||||
Caps(c) => {
|
||||
let format = c.caps().structure(0).map(|s| s.name().as_str());
|
||||
let mut state = self.state.lock().unwrap();
|
||||
|
||||
state.in_format = format.and_then(|f| match f {
|
||||
"text/x-raw" => Some(aws_sdk_polly::types::TextType::Text),
|
||||
"application/ssml+xml" => Some(aws_sdk_polly::types::TextType::Ssml),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
drop(state);
|
||||
|
||||
let caps = gst_audio::AudioCapsBuilder::new()
|
||||
.format(gst_audio::AudioFormat::S16le)
|
||||
.rate(16_000)
|
||||
.channels(1)
|
||||
.layout(gst_audio::AudioLayout::Interleaved)
|
||||
.build();
|
||||
|
||||
let event = gst::event::Caps::builder(&caps).seqnum(c.seqnum()).build();
|
||||
|
||||
self.srcpad.push_event(event)
|
||||
}
|
||||
Gap(g) => {
|
||||
let (pts, duration) = g.get();
|
||||
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.out_segment.set_position(match duration {
|
||||
Some(duration) => duration + pts,
|
||||
_ => pts,
|
||||
});
|
||||
drop(state);
|
||||
gst::Pad::event_default(pad, Some(&*self.obj()), event)
|
||||
}
|
||||
_ => gst::Pad::event_default(pad, Some(&*self.obj()), event),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(&self, inbuf: gst::Buffer) -> Result<gst::Buffer, Error> {
|
||||
let pts = inbuf
|
||||
.pts()
|
||||
.ok_or_else(|| anyhow!("Stream with timestamped buffers required"))?;
|
||||
|
||||
let duration = inbuf
|
||||
.duration()
|
||||
.ok_or_else(|| anyhow!("Buffers of stream need to have a duration"))?;
|
||||
|
||||
let data = inbuf
|
||||
.map_readable()
|
||||
.map_err(|_| anyhow!("Can't map buffer readable"))?;
|
||||
|
||||
let data =
|
||||
std::str::from_utf8(&data).map_err(|err| anyhow!("Can't decode utf8: {}", err))?;
|
||||
|
||||
let (client, in_format) = {
|
||||
let state = self.state.lock().unwrap();
|
||||
|
||||
(
|
||||
state.client.as_ref().expect("connected").clone(),
|
||||
state.in_format.as_ref().expect("received caps").clone(),
|
||||
)
|
||||
};
|
||||
|
||||
let job = {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
|
||||
let mut task = client
|
||||
.synthesize_speech()
|
||||
.engine(settings.engine.into())
|
||||
.output_format(aws_sdk_polly::types::OutputFormat::Pcm)
|
||||
.text_type(in_format)
|
||||
.text(data)
|
||||
.voice_id(settings.voice_id.into())
|
||||
.set_lexicon_names(Some(
|
||||
settings
|
||||
.lexicon_names
|
||||
.iter()
|
||||
.map(|v| v.get::<String>().unwrap())
|
||||
.collect(),
|
||||
));
|
||||
|
||||
if settings.language_code != AwsPollyLanguageCode::None {
|
||||
task = task.language_code(settings.language_code.into());
|
||||
}
|
||||
|
||||
task.send()
|
||||
};
|
||||
|
||||
let resp = job.await.map_err(|err| {
|
||||
if let Some(err) = err.as_service_error() {
|
||||
gst::error!(CAT, imp = self, "Failed sending text chunk: {}", err.meta());
|
||||
} else {
|
||||
gst::error!(CAT, imp = self, "Failed sending text chunk: {}", err);
|
||||
}
|
||||
err
|
||||
})?;
|
||||
let blob = resp.audio_stream.collect().await?;
|
||||
|
||||
let mut buf = gst::Buffer::from_slice(blob.into_bytes());
|
||||
let mut state = self.state.lock().unwrap();
|
||||
|
||||
let discont = state
|
||||
.out_segment
|
||||
.position()
|
||||
.map(|position| position < pts + duration)
|
||||
.unwrap_or(true);
|
||||
|
||||
{
|
||||
let buf_mut = buf.get_mut().unwrap();
|
||||
buf_mut.set_pts(pts);
|
||||
buf_mut.set_duration(duration);
|
||||
|
||||
if discont {
|
||||
gst::log!(CAT, imp = self, "Marking buffer discont");
|
||||
buf_mut.set_flags(gst::BufferFlags::DISCONT);
|
||||
}
|
||||
inbuf.foreach_meta(|meta| {
|
||||
if meta.tags().is_empty() {
|
||||
if let Err(err) =
|
||||
meta.transform(buf_mut, &gst::meta::MetaTransformCopy::new(false, ..))
|
||||
{
|
||||
gst::trace!(CAT, imp = self, "Could not copy meta {}: {err}", meta.api());
|
||||
}
|
||||
}
|
||||
std::ops::ControlFlow::Continue(())
|
||||
});
|
||||
}
|
||||
|
||||
state.out_segment.set_position(pts + duration);
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn sink_chain(
|
||||
&self,
|
||||
pad: &gst::Pad,
|
||||
buffer: gst::Buffer,
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
gst::log!(CAT, obj = pad, "Handling {buffer:?}");
|
||||
|
||||
self.ensure_connection().map_err(|err| {
|
||||
gst::element_imp_error!(self, gst::StreamError::Failed, ["Streaming failed: {err}"]);
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
let (future, abort_handle) = abortable(self.send(buffer));
|
||||
|
||||
self.state.lock().unwrap().send_abort_handle = Some(abort_handle);
|
||||
|
||||
match RUNTIME.block_on(future) {
|
||||
Err(_) => {
|
||||
gst::debug!(CAT, imp = self, "send aborted, returning flushing");
|
||||
Err(gst::FlowError::Flushing)
|
||||
}
|
||||
Ok(res) => match res {
|
||||
Err(e) => {
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::StreamError::Failed,
|
||||
["Failed sending data: {}", e]
|
||||
);
|
||||
Err(gst::FlowError::Error)
|
||||
}
|
||||
Ok(buf) => self.srcpad.push(buf),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_connection(&self) -> Result<(), gst::ErrorMessage> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if state.client.is_none() {
|
||||
state.client = Some(aws_sdk_polly::Client::new(
|
||||
self.aws_config.lock().unwrap().as_ref().expect("prepared"),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare(&self) -> Result<(), gst::ErrorMessage> {
|
||||
gst::debug!(CAT, imp = self, "Preparing");
|
||||
|
||||
let (access_key, secret_access_key, session_token) = {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
(
|
||||
settings.access_key.clone(),
|
||||
settings.secret_access_key.clone(),
|
||||
settings.session_token.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
gst::info!(CAT, imp = self, "Loading aws config...");
|
||||
let _enter_guard = RUNTIME.enter();
|
||||
|
||||
let config_loader = match (access_key, secret_access_key) {
|
||||
(Some(key), Some(secret_key)) => {
|
||||
gst::debug!(CAT, imp = self, "Using settings credentials");
|
||||
aws_config::defaults(*AWS_BEHAVIOR_VERSION).credentials_provider(
|
||||
aws_sdk_polly::config::Credentials::new(
|
||||
key,
|
||||
secret_key,
|
||||
session_token,
|
||||
None,
|
||||
"translate",
|
||||
),
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
gst::debug!(CAT, imp = self, "Attempting to get credentials from env...");
|
||||
aws_config::defaults(*AWS_BEHAVIOR_VERSION)
|
||||
}
|
||||
};
|
||||
|
||||
let config_loader = config_loader.region(
|
||||
aws_config::meta::region::RegionProviderChain::default_provider()
|
||||
.or_else(DEFAULT_REGION),
|
||||
);
|
||||
|
||||
let config_loader =
|
||||
config_loader.stalled_stream_protection(StalledStreamProtectionConfig::disabled());
|
||||
|
||||
let config = futures::executor::block_on(config_loader.load());
|
||||
gst::debug!(CAT, imp = self, "Using region {}", config.region().unwrap());
|
||||
|
||||
*self.aws_config.lock().unwrap() = Some(config);
|
||||
|
||||
gst::debug!(CAT, imp = self, "Prepared");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn disconnect(&self) {
|
||||
gst::info!(CAT, imp = self, "Disconnecting");
|
||||
let mut state = self.state.lock().unwrap();
|
||||
|
||||
if let Some(abort_handle) = state.send_abort_handle.take() {
|
||||
abort_handle.abort();
|
||||
}
|
||||
|
||||
*state = State::default();
|
||||
gst::info!(CAT, imp = self, "Disconnected");
|
||||
}
|
||||
|
||||
fn src_query(&self, pad: &gst::Pad, query: &mut gst::QueryRef) -> bool {
|
||||
gst::log!(CAT, obj = pad, "Handling query {:?}", query);
|
||||
|
||||
match query.view_mut() {
|
||||
gst::QueryViewMut::Latency(ref mut q) => {
|
||||
let mut peer_query = gst::query::Latency::new();
|
||||
|
||||
let ret = self.sinkpad.peer_query(&mut peer_query);
|
||||
|
||||
if ret {
|
||||
let (live, min, max) = peer_query.result();
|
||||
let our_latency = self.settings.lock().unwrap().latency;
|
||||
|
||||
if live {
|
||||
q.set(true, min + our_latency, max.map(|max| max + our_latency));
|
||||
} else {
|
||||
q.set(live, min, max);
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
gst::QueryViewMut::Position(ref mut q) => {
|
||||
if q.format() == gst::Format::Time {
|
||||
let state = self.state.lock().unwrap();
|
||||
q.set(
|
||||
state
|
||||
.out_segment
|
||||
.to_stream_time(state.out_segment.position()),
|
||||
);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
_ => gst::Pad::query_default(pad, Some(&*self.obj()), query),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for Polly {
|
||||
const NAME: &'static str = "GstAwsPolly";
|
||||
type Type = super::Polly;
|
||||
type ParentType = gst::Element;
|
||||
|
||||
fn with_class(klass: &Self::Class) -> Self {
|
||||
let templ = klass.pad_template("sink").unwrap();
|
||||
let sinkpad = gst::Pad::builder_from_template(&templ)
|
||||
.chain_function(|pad, parent, buffer| {
|
||||
Polly::catch_panic_pad_function(
|
||||
parent,
|
||||
|| Err(gst::FlowError::Error),
|
||||
|polly| polly.sink_chain(pad, buffer),
|
||||
)
|
||||
})
|
||||
.event_function(|pad, parent, event| {
|
||||
Polly::catch_panic_pad_function(
|
||||
parent,
|
||||
|| false,
|
||||
|polly| polly.sink_event(pad, event),
|
||||
)
|
||||
})
|
||||
.build();
|
||||
|
||||
let templ = klass.pad_template("src").unwrap();
|
||||
let srcpad = gst::PadBuilder::<gst::Pad>::from_template(&templ)
|
||||
.query_function(|pad, parent, query| {
|
||||
Polly::catch_panic_pad_function(
|
||||
parent,
|
||||
|| false,
|
||||
|polly| polly.src_query(pad, query),
|
||||
)
|
||||
})
|
||||
.flags(gst::PadFlags::FIXED_CAPS)
|
||||
.build();
|
||||
|
||||
Self {
|
||||
srcpad,
|
||||
sinkpad,
|
||||
settings: Default::default(),
|
||||
state: Default::default(),
|
||||
aws_config: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for Polly {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: LazyLock<Vec<glib::ParamSpec>> = LazyLock::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecUInt::builder("latency")
|
||||
.nick("Latency")
|
||||
.blurb("Amount of milliseconds to allow AWS Polly")
|
||||
.default_value(DEFAULT_LATENCY.mseconds() as u32)
|
||||
.mutable_ready()
|
||||
.deprecated()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("access-key")
|
||||
.nick("Access Key")
|
||||
.blurb("AWS Access Key")
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("secret-access-key")
|
||||
.nick("Secret Access Key")
|
||||
.blurb("AWS Secret Access Key")
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("session-token")
|
||||
.nick("Session Token")
|
||||
.blurb("AWS temporary Session Token from STS")
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecEnum::builder_with_default("engine", DEFAULT_ENGINE)
|
||||
.nick("Engine")
|
||||
.blurb("Defines what engine to use")
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecEnum::builder_with_default("voice-id", DEFAULT_VOICE_ID)
|
||||
.nick("Voice Id")
|
||||
.blurb("Defines what voice id to use")
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecEnum::builder_with_default("language-code", DEFAULT_LANGUAGE_CODE)
|
||||
.nick("Language Code")
|
||||
.blurb("Defines what language code to use")
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
gst::ParamSpecArray::builder("lexicon-names")
|
||||
.nick("Lexicon Names")
|
||||
.blurb("List of lexicon names to use")
|
||||
.element_spec(
|
||||
&glib::ParamSpecString::builder("lexicon-name")
|
||||
.nick("Lexicon Name")
|
||||
.blurb("The lexicon name")
|
||||
.build(),
|
||||
)
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
let obj = self.obj();
|
||||
obj.add_pad(&self.sinkpad).unwrap();
|
||||
obj.add_pad(&self.srcpad).unwrap();
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"latency" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.latency = gst::ClockTime::from_mseconds(
|
||||
value.get::<u32>().expect("type checked upstream").into(),
|
||||
);
|
||||
}
|
||||
"access-key" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.access_key = value.get().expect("type checked upstream");
|
||||
}
|
||||
"secret-access-key" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.secret_access_key = value.get().expect("type checked upstream");
|
||||
}
|
||||
"session-token" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.session_token = value.get().expect("type checked upstream");
|
||||
}
|
||||
"engine" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.engine = value
|
||||
.get::<AwsPollyEngine>()
|
||||
.expect("type checked upstream");
|
||||
}
|
||||
"voice-id" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.voice_id = value
|
||||
.get::<AwsPollyVoiceId>()
|
||||
.expect("type checked upstream");
|
||||
}
|
||||
"language-code" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.language_code = value
|
||||
.get::<AwsPollyLanguageCode>()
|
||||
.expect("type checked upstream");
|
||||
}
|
||||
"lexicon-names" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.lexicon_names = value.get::<gst::Array>().expect("type checked upstream");
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"latency" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
(settings.latency.mseconds() as u32).to_value()
|
||||
}
|
||||
"access-key" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.access_key.to_value()
|
||||
}
|
||||
"secret-access-key" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.secret_access_key.to_value()
|
||||
}
|
||||
"session-token" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.session_token.to_value()
|
||||
}
|
||||
"engine" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.engine.to_value()
|
||||
}
|
||||
"voice-id" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.voice_id.to_value()
|
||||
}
|
||||
"language-code" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.language_code.to_value()
|
||||
}
|
||||
"lexicon-names" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.lexicon_names.to_value()
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GstObjectImpl for Polly {}
|
||||
|
||||
impl ElementImpl for Polly {
|
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||
static ELEMENT_METADATA: LazyLock<gst::subclass::ElementMetadata> = LazyLock::new(|| {
|
||||
gst::subclass::ElementMetadata::new(
|
||||
"Polly",
|
||||
"Audio/Text/Filter",
|
||||
"Text to Speech filter, using AWS polly",
|
||||
"Mathieu Duponchelle <mathieu@centricular.com>",
|
||||
)
|
||||
});
|
||||
|
||||
Some(&*ELEMENT_METADATA)
|
||||
}
|
||||
|
||||
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||
static PAD_TEMPLATES: LazyLock<Vec<gst::PadTemplate>> = LazyLock::new(|| {
|
||||
let sink_caps = gst::Caps::builder_full()
|
||||
.structure(
|
||||
gst::Structure::builder("text/x-raw")
|
||||
.field("format", "utf8")
|
||||
.build(),
|
||||
)
|
||||
.structure(gst::Structure::new_empty("application/ssml+xml"))
|
||||
.build();
|
||||
let sink_pad_template = gst::PadTemplate::new(
|
||||
"sink",
|
||||
gst::PadDirection::Sink,
|
||||
gst::PadPresence::Always,
|
||||
&sink_caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let src_caps = gst_audio::AudioCapsBuilder::new()
|
||||
.format(gst_audio::AudioFormat::S16le)
|
||||
.rate(16_000)
|
||||
.channels(1)
|
||||
.layout(gst_audio::AudioLayout::Interleaved)
|
||||
.build();
|
||||
let src_pad_template = gst::PadTemplate::new(
|
||||
"src",
|
||||
gst::PadDirection::Src,
|
||||
gst::PadPresence::Always,
|
||||
&src_caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vec![src_pad_template, sink_pad_template]
|
||||
});
|
||||
|
||||
PAD_TEMPLATES.as_ref()
|
||||
}
|
||||
|
||||
fn change_state(
|
||||
&self,
|
||||
transition: gst::StateChange,
|
||||
) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
|
||||
gst::info!(CAT, imp = self, "Changing state {transition:?}");
|
||||
|
||||
match transition {
|
||||
gst::StateChange::NullToReady => {
|
||||
self.prepare().map_err(|err| {
|
||||
self.post_error_message(err);
|
||||
gst::StateChangeError
|
||||
})?;
|
||||
}
|
||||
gst::StateChange::PausedToReady => {
|
||||
self.disconnect();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.parent_change_state(transition)
|
||||
}
|
||||
|
||||
fn provide_clock(&self) -> Option<gst::Clock> {
|
||||
Some(gst::SystemClock::obtain())
|
||||
}
|
||||
}
|
418
net/aws/src/polly/mod.rs
Normal file
418
net/aws/src/polly/mod.rs
Normal file
|
@ -0,0 +1,418 @@
|
|||
// Copyright (C) 2024 Mathieu Duponchelle <mathieu@centricular.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
mod imp;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
static CAT: LazyLock<gst::DebugCategory> = LazyLock::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
"awspolly",
|
||||
gst::DebugColorFlags::empty(),
|
||||
Some("AWS Polly element"),
|
||||
)
|
||||
});
|
||||
|
||||
use aws_sdk_polly::types::{Engine, LanguageCode, VoiceId};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)]
|
||||
#[repr(u32)]
|
||||
#[enum_type(name = "GstAwsPollyEngine")]
|
||||
#[non_exhaustive]
|
||||
pub enum AwsPollyEngine {
|
||||
#[enum_value(name = "Standard", nick = "standard")]
|
||||
Standard = 0,
|
||||
#[enum_value(name = "Neural", nick = "neural")]
|
||||
Neural = 1,
|
||||
}
|
||||
|
||||
impl From<AwsPollyEngine> for Engine {
|
||||
fn from(val: AwsPollyEngine) -> Self {
|
||||
use AwsPollyEngine::*;
|
||||
match val {
|
||||
Standard => Engine::Standard,
|
||||
Neural => Engine::Neural,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)]
|
||||
#[repr(u32)]
|
||||
#[enum_type(name = "GstAwsPollyVoiceId")]
|
||||
#[non_exhaustive]
|
||||
pub enum AwsPollyVoiceId {
|
||||
#[enum_value(name = "Aditi", nick = "aditi")]
|
||||
Aditi,
|
||||
#[enum_value(name = "Amy", nick = "amy")]
|
||||
Amy,
|
||||
#[enum_value(name = "Aria", nick = "aria")]
|
||||
Aria,
|
||||
#[enum_value(name = "Arlet", nick = "arlet")]
|
||||
Arlet,
|
||||
#[enum_value(name = "Arthur", nick = "arthur")]
|
||||
Arthur,
|
||||
#[enum_value(name = "Astrid", nick = "astrid")]
|
||||
Astrid,
|
||||
#[enum_value(name = "Ayanda", nick = "ayanda")]
|
||||
Ayanda,
|
||||
#[enum_value(name = "Bianca", nick = "bianca")]
|
||||
Bianca,
|
||||
#[enum_value(name = "Brian", nick = "brian")]
|
||||
Brian,
|
||||
#[enum_value(name = "Camila", nick = "camila")]
|
||||
Camila,
|
||||
#[enum_value(name = "Carla", nick = "carla")]
|
||||
Carla,
|
||||
#[enum_value(name = "Carmen", nick = "carmen")]
|
||||
Carmen,
|
||||
#[enum_value(name = "Celine", nick = "celine")]
|
||||
Celine,
|
||||
#[enum_value(name = "Chantal", nick = "chantal")]
|
||||
Chantal,
|
||||
#[enum_value(name = "Conchita", nick = "conchita")]
|
||||
Conchita,
|
||||
#[enum_value(name = "Cristiano", nick = "cristiano")]
|
||||
Cristiano,
|
||||
#[enum_value(name = "Daniel", nick = "daniel")]
|
||||
Daniel,
|
||||
#[enum_value(name = "Dora", nick = "dora")]
|
||||
Dora,
|
||||
#[enum_value(name = "Emma", nick = "emma")]
|
||||
Emma,
|
||||
#[enum_value(name = "Enrique", nick = "enrique")]
|
||||
Enrique,
|
||||
#[enum_value(name = "Ewa", nick = "ewa")]
|
||||
Ewa,
|
||||
#[enum_value(name = "Filiz", nick = "filiz")]
|
||||
Filiz,
|
||||
#[enum_value(name = "Gabrielle", nick = "gabrielle")]
|
||||
Gabrielle,
|
||||
#[enum_value(name = "Geraint", nick = "geraint")]
|
||||
Geraint,
|
||||
#[enum_value(name = "Giorgio", nick = "giorgio")]
|
||||
Giorgio,
|
||||
#[enum_value(name = "Gwyneth", nick = "gwyneth")]
|
||||
Gwyneth,
|
||||
#[enum_value(name = "Hannah", nick = "hannah")]
|
||||
Hannah,
|
||||
#[enum_value(name = "Hans", nick = "hans")]
|
||||
Hans,
|
||||
#[enum_value(name = "Hiujin", nick = "hiujin")]
|
||||
Hiujin,
|
||||
#[enum_value(name = "Ines", nick = "ines")]
|
||||
Ines,
|
||||
#[enum_value(name = "Ivy", nick = "ivy")]
|
||||
Ivy,
|
||||
#[enum_value(name = "Jacek", nick = "jacek")]
|
||||
Jacek,
|
||||
#[enum_value(name = "Jan", nick = "jan")]
|
||||
Jan,
|
||||
#[enum_value(name = "Joanna", nick = "joanna")]
|
||||
Joanna,
|
||||
#[enum_value(name = "Joey", nick = "joey")]
|
||||
Joey,
|
||||
#[enum_value(name = "Justin", nick = "justin")]
|
||||
Justin,
|
||||
#[enum_value(name = "Kajal", nick = "kajal")]
|
||||
Kajal,
|
||||
#[enum_value(name = "Karl", nick = "karl")]
|
||||
Karl,
|
||||
#[enum_value(name = "Kendra", nick = "kendra")]
|
||||
Kendra,
|
||||
#[enum_value(name = "Kevin", nick = "kevin")]
|
||||
Kevin,
|
||||
#[enum_value(name = "Kimberly", nick = "kimberly")]
|
||||
Kimberly,
|
||||
#[enum_value(name = "Lea", nick = "lea")]
|
||||
Lea,
|
||||
#[enum_value(name = "Liam", nick = "liam")]
|
||||
Liam,
|
||||
#[enum_value(name = "Liv", nick = "liv")]
|
||||
Liv,
|
||||
#[enum_value(name = "Lotte", nick = "lotte")]
|
||||
Lotte,
|
||||
#[enum_value(name = "Lucia", nick = "lucia")]
|
||||
Lucia,
|
||||
#[enum_value(name = "Lupe", nick = "lupe")]
|
||||
Lupe,
|
||||
#[enum_value(name = "Mads", nick = "mads")]
|
||||
Mads,
|
||||
#[enum_value(name = "Maja", nick = "maja")]
|
||||
Maja,
|
||||
#[enum_value(name = "Marlene", nick = "marlene")]
|
||||
Marlene,
|
||||
#[enum_value(name = "Mathieu", nick = "mathieu")]
|
||||
Mathieu,
|
||||
#[enum_value(name = "Matthew", nick = "matthew")]
|
||||
Matthew,
|
||||
#[enum_value(name = "Maxim", nick = "maxim")]
|
||||
Maxim,
|
||||
#[enum_value(name = "Mia", nick = "mia")]
|
||||
Mia,
|
||||
#[enum_value(name = "Miguel", nick = "miguel")]
|
||||
Miguel,
|
||||
#[enum_value(name = "Mizuki", nick = "mizuki")]
|
||||
Mizuki,
|
||||
#[enum_value(name = "Naja", nick = "naja")]
|
||||
Naja,
|
||||
#[enum_value(name = "Nicole", nick = "nicole")]
|
||||
Nicole,
|
||||
#[enum_value(name = "Olivia", nick = "olivia")]
|
||||
Olivia,
|
||||
#[enum_value(name = "Pedro", nick = "pedro")]
|
||||
Pedro,
|
||||
#[enum_value(name = "Penelope", nick = "penelope")]
|
||||
Penelope,
|
||||
#[enum_value(name = "Raveena", nick = "raveena")]
|
||||
Raveena,
|
||||
#[enum_value(name = "Ricardo", nick = "ricardo")]
|
||||
Ricardo,
|
||||
#[enum_value(name = "Ruben", nick = "ruben")]
|
||||
Ruben,
|
||||
#[enum_value(name = "Russell", nick = "russell")]
|
||||
Russell,
|
||||
#[enum_value(name = "Salli", nick = "salli")]
|
||||
Salli,
|
||||
#[enum_value(name = "Seoyeon", nick = "seoyeon")]
|
||||
Seoyeon,
|
||||
#[enum_value(name = "Takumi", nick = "takumi")]
|
||||
Takumi,
|
||||
#[enum_value(name = "Tatyana", nick = "tatyana")]
|
||||
Tatyana,
|
||||
#[enum_value(name = "Vicki", nick = "vicki")]
|
||||
Vicki,
|
||||
#[enum_value(name = "Vitoria", nick = "vitoria")]
|
||||
Vitoria,
|
||||
#[enum_value(name = "Zeina", nick = "zeina")]
|
||||
Zeina,
|
||||
#[enum_value(name = "Zhiyu", nick = "zhiyu")]
|
||||
Zhiyu,
|
||||
}
|
||||
|
||||
impl From<AwsPollyVoiceId> for VoiceId {
|
||||
fn from(val: AwsPollyVoiceId) -> Self {
|
||||
use AwsPollyVoiceId::*;
|
||||
match val {
|
||||
Aditi => VoiceId::Aditi,
|
||||
Amy => VoiceId::Amy,
|
||||
Aria => VoiceId::Aria,
|
||||
Arlet => VoiceId::Arlet,
|
||||
Arthur => VoiceId::Arthur,
|
||||
Astrid => VoiceId::Astrid,
|
||||
Ayanda => VoiceId::Ayanda,
|
||||
Bianca => VoiceId::Bianca,
|
||||
Brian => VoiceId::Brian,
|
||||
Camila => VoiceId::Camila,
|
||||
Carla => VoiceId::Carla,
|
||||
Carmen => VoiceId::Carmen,
|
||||
Celine => VoiceId::Celine,
|
||||
Chantal => VoiceId::Chantal,
|
||||
Conchita => VoiceId::Conchita,
|
||||
Cristiano => VoiceId::Cristiano,
|
||||
Daniel => VoiceId::Daniel,
|
||||
Dora => VoiceId::Dora,
|
||||
Emma => VoiceId::Emma,
|
||||
Enrique => VoiceId::Enrique,
|
||||
Ewa => VoiceId::Ewa,
|
||||
Filiz => VoiceId::Filiz,
|
||||
Gabrielle => VoiceId::Gabrielle,
|
||||
Geraint => VoiceId::Geraint,
|
||||
Giorgio => VoiceId::Giorgio,
|
||||
Gwyneth => VoiceId::Gwyneth,
|
||||
Hannah => VoiceId::Hannah,
|
||||
Hans => VoiceId::Hans,
|
||||
Hiujin => VoiceId::Hiujin,
|
||||
Ines => VoiceId::Ines,
|
||||
Ivy => VoiceId::Ivy,
|
||||
Jacek => VoiceId::Jacek,
|
||||
Jan => VoiceId::Jan,
|
||||
Joanna => VoiceId::Joanna,
|
||||
Joey => VoiceId::Joey,
|
||||
Justin => VoiceId::Justin,
|
||||
Kajal => VoiceId::Kajal,
|
||||
Karl => VoiceId::Karl,
|
||||
Kendra => VoiceId::Kendra,
|
||||
Kevin => VoiceId::Kevin,
|
||||
Kimberly => VoiceId::Kimberly,
|
||||
Lea => VoiceId::Lea,
|
||||
Liam => VoiceId::Liam,
|
||||
Liv => VoiceId::Liv,
|
||||
Lotte => VoiceId::Lotte,
|
||||
Lucia => VoiceId::Lucia,
|
||||
Lupe => VoiceId::Lupe,
|
||||
Mads => VoiceId::Mads,
|
||||
Maja => VoiceId::Maja,
|
||||
Marlene => VoiceId::Marlene,
|
||||
Mathieu => VoiceId::Mathieu,
|
||||
Matthew => VoiceId::Matthew,
|
||||
Maxim => VoiceId::Maxim,
|
||||
Mia => VoiceId::Mia,
|
||||
Miguel => VoiceId::Miguel,
|
||||
Mizuki => VoiceId::Mizuki,
|
||||
Naja => VoiceId::Naja,
|
||||
Nicole => VoiceId::Nicole,
|
||||
Olivia => VoiceId::Olivia,
|
||||
Pedro => VoiceId::Pedro,
|
||||
Penelope => VoiceId::Penelope,
|
||||
Raveena => VoiceId::Raveena,
|
||||
Ricardo => VoiceId::Ricardo,
|
||||
Ruben => VoiceId::Ruben,
|
||||
Russell => VoiceId::Russell,
|
||||
Salli => VoiceId::Salli,
|
||||
Seoyeon => VoiceId::Seoyeon,
|
||||
Takumi => VoiceId::Takumi,
|
||||
Tatyana => VoiceId::Tatyana,
|
||||
Vicki => VoiceId::Vicki,
|
||||
Vitoria => VoiceId::Vitoria,
|
||||
Zeina => VoiceId::Zeina,
|
||||
Zhiyu => VoiceId::Zhiyu,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)]
|
||||
#[repr(u32)]
|
||||
#[enum_type(name = "GstAwsPollyLanguageCode")]
|
||||
#[non_exhaustive]
|
||||
pub enum AwsPollyLanguageCode {
|
||||
#[enum_value(name = "None", nick = "none")]
|
||||
None,
|
||||
#[enum_value(name = "Arb", nick = "arb")]
|
||||
Arb,
|
||||
#[enum_value(name = "CaEs", nick = "ca-ES")]
|
||||
CaEs,
|
||||
#[enum_value(name = "CmnCn", nick = "cmn-CN")]
|
||||
CmnCn,
|
||||
#[enum_value(name = "CyGb", nick = "cy-GB")]
|
||||
CyGb,
|
||||
#[enum_value(name = "DaDk", nick = "da-DK")]
|
||||
DaDk,
|
||||
#[enum_value(name = "DeAt", nick = "de-AT")]
|
||||
DeAt,
|
||||
#[enum_value(name = "DeDe", nick = "de-DE")]
|
||||
DeDe,
|
||||
#[enum_value(name = "EnAu", nick = "en-AU")]
|
||||
EnAu,
|
||||
#[enum_value(name = "EnGb", nick = "en-GB")]
|
||||
EnGb,
|
||||
#[enum_value(name = "EnGbWls", nick = "en-GB-WLS")]
|
||||
EnGbWls,
|
||||
#[enum_value(name = "EnIn", nick = "en-IN")]
|
||||
EnIn,
|
||||
#[enum_value(name = "EnNz", nick = "en-NZ")]
|
||||
EnNz,
|
||||
#[enum_value(name = "EnUs", nick = "en-US")]
|
||||
EnUs,
|
||||
#[enum_value(name = "EnZa", nick = "en-ZA")]
|
||||
EnZa,
|
||||
#[enum_value(name = "EsEs", nick = "es-ES")]
|
||||
EsEs,
|
||||
#[enum_value(name = "EsMx", nick = "es-MX")]
|
||||
EsMx,
|
||||
#[enum_value(name = "EsUs", nick = "es-US")]
|
||||
EsUs,
|
||||
#[enum_value(name = "FrCa", nick = "fr-CA")]
|
||||
FrCa,
|
||||
#[enum_value(name = "FrFr", nick = "fr-FR")]
|
||||
FrFr,
|
||||
#[enum_value(name = "HiIn", nick = "hi-IN")]
|
||||
HiIn,
|
||||
#[enum_value(name = "IsIs", nick = "is-IS")]
|
||||
IsIs,
|
||||
#[enum_value(name = "ItIt", nick = "it-IT")]
|
||||
ItIt,
|
||||
#[enum_value(name = "JaJp", nick = "ja-JP")]
|
||||
JaJp,
|
||||
#[enum_value(name = "KoKr", nick = "ko-KR")]
|
||||
KoKr,
|
||||
#[enum_value(name = "NbNo", nick = "nb-NO")]
|
||||
NbNo,
|
||||
#[enum_value(name = "NlNl", nick = "nl-NL")]
|
||||
NlNl,
|
||||
#[enum_value(name = "PlPl", nick = "pl-PL")]
|
||||
PlPl,
|
||||
#[enum_value(name = "PtBr", nick = "pt-BR")]
|
||||
PtBr,
|
||||
#[enum_value(name = "PtPt", nick = "pt-PT")]
|
||||
PtPt,
|
||||
#[enum_value(name = "RoRo", nick = "ro-RO")]
|
||||
RoRo,
|
||||
#[enum_value(name = "RuRu", nick = "ru-RU")]
|
||||
RuRu,
|
||||
#[enum_value(name = "SvSe", nick = "sv-SE")]
|
||||
SvSe,
|
||||
#[enum_value(name = "TrTr", nick = "tr-TR")]
|
||||
TrTr,
|
||||
#[enum_value(name = "YueCn", nick = "yue-CN")]
|
||||
YueCn,
|
||||
}
|
||||
|
||||
impl From<AwsPollyLanguageCode> for LanguageCode {
|
||||
fn from(val: AwsPollyLanguageCode) -> Self {
|
||||
use AwsPollyLanguageCode::*;
|
||||
match val {
|
||||
Arb => LanguageCode::Arb,
|
||||
CaEs => LanguageCode::CaEs,
|
||||
CmnCn => LanguageCode::CmnCn,
|
||||
CyGb => LanguageCode::CyGb,
|
||||
DaDk => LanguageCode::DaDk,
|
||||
DeAt => LanguageCode::DeAt,
|
||||
DeDe => LanguageCode::DeDe,
|
||||
EnAu => LanguageCode::EnAu,
|
||||
EnGb => LanguageCode::EnGb,
|
||||
EnGbWls => LanguageCode::EnGbWls,
|
||||
EnIn => LanguageCode::EnIn,
|
||||
EnNz => LanguageCode::EnNz,
|
||||
EnUs => LanguageCode::EnUs,
|
||||
EnZa => LanguageCode::EnZa,
|
||||
EsEs => LanguageCode::EsEs,
|
||||
EsMx => LanguageCode::EsMx,
|
||||
EsUs => LanguageCode::EsUs,
|
||||
FrCa => LanguageCode::FrCa,
|
||||
FrFr => LanguageCode::FrFr,
|
||||
HiIn => LanguageCode::HiIn,
|
||||
IsIs => LanguageCode::IsIs,
|
||||
ItIt => LanguageCode::ItIt,
|
||||
JaJp => LanguageCode::JaJp,
|
||||
KoKr => LanguageCode::KoKr,
|
||||
NbNo => LanguageCode::NbNo,
|
||||
NlNl => LanguageCode::NlNl,
|
||||
PlPl => LanguageCode::PlPl,
|
||||
PtBr => LanguageCode::PtBr,
|
||||
PtPt => LanguageCode::PtPt,
|
||||
RoRo => LanguageCode::RoRo,
|
||||
RuRu => LanguageCode::RuRu,
|
||||
SvSe => LanguageCode::SvSe,
|
||||
TrTr => LanguageCode::TrTr,
|
||||
YueCn => LanguageCode::YueCn,
|
||||
None => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct Polly(ObjectSubclass<imp::Polly>) @extends gst::Element, gst::Object;
|
||||
}
|
||||
|
||||
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
#[cfg(feature = "doc")]
|
||||
{
|
||||
AwsPollyEngine::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
|
||||
AwsPollyVoiceId::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
|
||||
AwsPollyLanguageCode::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
|
||||
}
|
||||
gst::Element::register(
|
||||
Some(plugin),
|
||||
"awspolly",
|
||||
gst::Rank::NONE,
|
||||
Polly::static_type(),
|
||||
)
|
||||
}
|
|
@ -28,7 +28,7 @@ pub const DEFAULT_S3_REGION: &str = "us-west-2";
|
|||
pub static AWS_BEHAVIOR_VERSION: LazyLock<aws_config::BehaviorVersion> =
|
||||
LazyLock::new(aws_config::BehaviorVersion::v2023_11_09);
|
||||
|
||||
static RUNTIME: LazyLock<runtime::Runtime> = LazyLock::new(|| {
|
||||
pub static RUNTIME: LazyLock<runtime::Runtime> = LazyLock::new(|| {
|
||||
runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.worker_threads(2)
|
||||
|
|
|
@ -27,7 +27,7 @@ use aws_sdk_transcribestreaming as aws_transcribe;
|
|||
use futures::channel::mpsc;
|
||||
use futures::future::AbortHandle;
|
||||
use futures::prelude::*;
|
||||
use tokio::{runtime, sync::broadcast, task};
|
||||
use tokio::{sync::broadcast, task};
|
||||
|
||||
use std::collections::{BTreeSet, VecDeque};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
@ -40,13 +40,7 @@ use super::{
|
|||
AwsTranscriberResultStability, AwsTranscriberVocabularyFilterMethod,
|
||||
TranslationTokenizationMethod, CAT,
|
||||
};
|
||||
|
||||
static RUNTIME: LazyLock<runtime::Runtime> = LazyLock::new(|| {
|
||||
runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
use crate::s3utils::RUNTIME;
|
||||
|
||||
#[allow(deprecated)]
|
||||
static AWS_BEHAVIOR_VERSION: LazyLock<aws_config::BehaviorVersion> =
|
||||
|
|
|
@ -12,6 +12,10 @@ trun = "trun"
|
|||
# net/rtp/src/ac3 - "5/8ths" - not sure how to allow this without also letting through all typos of 'this'
|
||||
ths = "ths"
|
||||
|
||||
# net/aws
|
||||
ines = "ines"
|
||||
Ines = "Ines"
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
"*.mcc",
|
||||
|
|
Loading…
Reference in a new issue