Compare commits
10 Commits
03106830e5
...
8fad4c5033
Author | SHA1 | Date | |
---|---|---|---|
8fad4c5033
|
|||
ad886ea985
|
|||
9dd5f47010
|
|||
d957800e18
|
|||
900c7ecb51
|
|||
eb0dea6113
|
|||
22cbaf0bca
|
|||
bab563519c
|
|||
f6b5e835a4
|
|||
3f31bd5ff2
|
20
go.mod
20
go.mod
@@ -14,6 +14,8 @@ require (
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-sqlite3 v1.14.17
|
||||
github.com/pressly/goose/v3 v3.15.1
|
||||
github.com/prometheus/client_golang v1.23.0
|
||||
github.com/samber/slog-gin v1.15.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/yandex-cloud/go-genproto v0.17.0
|
||||
google.golang.org/grpc v1.74.2
|
||||
@@ -34,27 +36,35 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.36.0 // indirect
|
||||
github.com/aws/smithy-go v1.22.5 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.11.9 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/go-playground/validator/v10 v10.22.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
|
51
go.sum
51
go.sum
@@ -38,10 +38,14 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.36.0 h1:bRP/a9llXSSgDPk7Rqn5GD/DQCGo
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.36.0/go.mod h1:tgBsFzxwl65BWkuJ/x2EUs59bD4SfYKgikvFDJi1S58=
|
||||
github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
|
||||
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg=
|
||||
github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
@@ -54,8 +58,8 @@ github.com/doug-martin/goqu/v9 v9.19.0 h1:PD7t1X3tRcUiSdc5TEyOFKujZA5gs3VSA7wxSv
|
||||
github.com/doug-martin/goqu/v9 v9.19.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
|
||||
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
@@ -70,11 +74,11 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
|
||||
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
@@ -89,10 +93,18 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
@@ -108,14 +120,28 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pressly/goose/v3 v3.15.1 h1:dKaJ1SdLvS/+HtS8PzFT0KBEtICC1jewLXM+b3emlv8=
|
||||
github.com/pressly/goose/v3 v3.15.1/go.mod h1:0E3Yg/+EwYzO6Rz2P98MlClFgIcoujbVRs575yi3iIM=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/samber/slog-gin v1.15.1 h1:jsnfr+S5HQPlz9pFPA3tOmKW7wN/znyZiE6hncucrTM=
|
||||
github.com/samber/slog-gin v1.15.1/go.mod h1:mPAEinK/g2jPLauuWO11m3Q0Ca7aG4k9XjXjXY8IhMQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -148,6 +174,8 @@ go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFw
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
@@ -182,8 +210,9 @@ google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
72
internal/adapter/metaviewer/ffmpeg/ffmpeg.go
Normal file
72
internal/adapter/metaviewer/ffmpeg/ffmpeg.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
||||
"git.vakhrushev.me/av/transcriber/internal/contract"
|
||||
)
|
||||
|
||||
const ffprobeExecutable = "ffprobe"
|
||||
|
||||
type FfmpegMetaViewer struct {
|
||||
}
|
||||
|
||||
// ffprobeOutput представляет структуру JSON-ответа от ffprobe
|
||||
type ffprobeOutput struct {
|
||||
Format struct {
|
||||
Duration string `json:"duration"`
|
||||
} `json:"format"`
|
||||
}
|
||||
|
||||
func NewFfmpegMetaViewer() *FfmpegMetaViewer {
|
||||
return &FfmpegMetaViewer{}
|
||||
}
|
||||
|
||||
func (m *FfmpegMetaViewer) GetInfo(src string) (*contract.AudioInfo, error) {
|
||||
// Проверяем существование исходного файла
|
||||
if _, err := os.Stat(src); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("input file does not exist: %s", src)
|
||||
}
|
||||
|
||||
// Проверяем, что ffprobe доступен в системе
|
||||
if _, err := exec.LookPath(ffprobeExecutable); err != nil {
|
||||
return nil, fmt.Errorf("ffprobe not found in PATH: %w", err)
|
||||
}
|
||||
|
||||
// Создаем команду ffprobe для получения метаданных
|
||||
cmd := exec.Command(ffprobeExecutable,
|
||||
"-v", "quiet", // тихий режим (без лишнего вывода)
|
||||
"-print_format", "json", // вывод в формате JSON
|
||||
"-show_format", // показать информацию о формате
|
||||
src, // входной файл
|
||||
)
|
||||
|
||||
// Выполняем команду и получаем вывод
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ffprobe execution failed: %w", err)
|
||||
}
|
||||
|
||||
// Парсим JSON-ответ
|
||||
var probeResult ffprobeOutput
|
||||
if err := json.Unmarshal(output, &probeResult); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse ffprobe output: %w", err)
|
||||
}
|
||||
|
||||
// Конвертируем длительность из строки в секунды
|
||||
durationFloat, err := strconv.ParseFloat(probeResult.Format.Duration, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse duration: %w", err)
|
||||
}
|
||||
|
||||
// Округляем до целых секунд
|
||||
durationSeconds := int(durationFloat + 0.5) // +0.5 для правильного округления
|
||||
|
||||
return &contract.AudioInfo{
|
||||
Seconds: durationSeconds,
|
||||
}, nil
|
||||
}
|
22
internal/adapter/recognizer/memory.go
Normal file
22
internal/adapter/recognizer/memory.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package recognizer
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type MemoryAudioRecognizer struct{}
|
||||
|
||||
func (r *MemoryAudioRecognizer) Recognize(file io.Reader, fileName string) (operationID string, err error) {
|
||||
return uuid.NewString(), nil
|
||||
}
|
||||
|
||||
func (r *MemoryAudioRecognizer) GetRecognitionText(operationID string) (string, error) {
|
||||
return "Foo bar, Baz.", nil
|
||||
}
|
||||
|
||||
func (r *MemoryAudioRecognizer) CheckRecognitionStatus(operationID string) (*entity.RecognitionResult, error) {
|
||||
return entity.NewCompletedResult(), nil
|
||||
}
|
94
internal/adapter/recognizer/yandex/recognizer.go
Normal file
94
internal/adapter/recognizer/yandex/recognizer.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package yandex
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||
)
|
||||
|
||||
type YandexAudioRecognizerConfig struct {
|
||||
// s3
|
||||
Region string
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
BucketName string
|
||||
Endpoint string
|
||||
// speech kit
|
||||
ApiKey string
|
||||
FolderID string
|
||||
}
|
||||
|
||||
type YandexAudioRecognizerService struct {
|
||||
s3Sevice *yandexS3Service
|
||||
sttService *speechKitService
|
||||
}
|
||||
|
||||
func NewYandexAudioRecognizerService(cfg YandexAudioRecognizerConfig) (*YandexAudioRecognizerService, error) {
|
||||
s3, err := newYandexS3Service(s3Config{
|
||||
Region: cfg.Region,
|
||||
AccessKey: cfg.AccessKey,
|
||||
SecretKey: cfg.SecretKey,
|
||||
BucketName: cfg.BucketName,
|
||||
Endpoint: cfg.Endpoint,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stt, err := newSpeechKitService(speechKitConfig{
|
||||
ApiKey: cfg.ApiKey,
|
||||
FolderID: cfg.FolderID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &YandexAudioRecognizerService{
|
||||
s3Sevice: s3,
|
||||
sttService: stt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *YandexAudioRecognizerService) Close() error {
|
||||
return s.sttService.Close()
|
||||
}
|
||||
|
||||
func (s *YandexAudioRecognizerService) Recognize(file io.Reader, fileName string) (string, error) {
|
||||
|
||||
err := s.s3Sevice.uploadFile(file, fileName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
uri := s.s3Sevice.fileUrl(fileName)
|
||||
|
||||
opId, err := s.sttService.recognizeFileFromS3(uri)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return opId, nil
|
||||
}
|
||||
|
||||
func (s *YandexAudioRecognizerService) GetRecognitionText(operationID string) (string, error) {
|
||||
return s.sttService.getRecognitionText(operationID)
|
||||
}
|
||||
|
||||
func (s *YandexAudioRecognizerService) CheckRecognitionStatus(operationID string) (*entity.RecognitionResult, error) {
|
||||
operation, err := s.sttService.checkOperationStatus(operationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !operation.Done {
|
||||
return entity.NewInProgressResult(), nil
|
||||
}
|
||||
|
||||
if opErr := operation.GetError(); opErr != nil {
|
||||
errorText := fmt.Sprintf("operation failed: code %d, message: %s", opErr.Code, opErr.Message)
|
||||
return entity.NewFailedResult(errorText), nil
|
||||
}
|
||||
|
||||
return entity.NewCompletedResult(), nil
|
||||
}
|
84
internal/adapter/recognizer/yandex/s3.go
Normal file
84
internal/adapter/recognizer/yandex/s3.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package yandex
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
)
|
||||
|
||||
type s3Config struct {
|
||||
Region string
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
BucketName string
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
type yandexS3Service struct {
|
||||
client *s3.Client
|
||||
uploader *manager.Uploader
|
||||
bucketName string
|
||||
endpoint string
|
||||
}
|
||||
|
||||
func newYandexS3Service(cfg s3Config) (*yandexS3Service, error) {
|
||||
if cfg.Region == "" || cfg.AccessKey == "" || cfg.SecretKey == "" || cfg.BucketName == "" {
|
||||
return nil, fmt.Errorf("missing required S3 configuration parameters")
|
||||
}
|
||||
|
||||
// Создаем конфигурацию
|
||||
awsCfg, err := config.LoadDefaultConfig(context.Background(),
|
||||
config.WithRegion(cfg.Region),
|
||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AccessKey, cfg.SecretKey, "")),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load AWS config: %w", err)
|
||||
}
|
||||
|
||||
// Создаем клиент S3
|
||||
var client *s3.Client
|
||||
if cfg.Endpoint != "" {
|
||||
// Кастомный endpoint (например, для MinIO)
|
||||
client = s3.NewFromConfig(awsCfg, func(o *s3.Options) {
|
||||
o.BaseEndpoint = aws.String(cfg.Endpoint)
|
||||
o.UsePathStyle = true
|
||||
})
|
||||
} else {
|
||||
// Стандартный AWS S3
|
||||
client = s3.NewFromConfig(awsCfg)
|
||||
}
|
||||
|
||||
uploader := manager.NewUploader(client)
|
||||
|
||||
return &yandexS3Service{
|
||||
client: client,
|
||||
uploader: uploader,
|
||||
bucketName: cfg.BucketName,
|
||||
endpoint: cfg.Endpoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *yandexS3Service) uploadFile(file io.Reader, fileName string) error {
|
||||
_, err := s.uploader.Upload(context.Background(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(s.bucketName),
|
||||
Key: aws.String(fileName),
|
||||
Body: file,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload file to S3: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *yandexS3Service) fileUrl(fileName string) string {
|
||||
endpoint := strings.TrimRight(s.endpoint, "/")
|
||||
return fmt.Sprintf("%s/%s/%s", endpoint, s.bucketName, fileName)
|
||||
}
|
@@ -1,9 +1,8 @@
|
||||
package speechkit
|
||||
package yandex
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
@@ -21,7 +20,12 @@ const (
|
||||
RecognitionModel = "deferred-general"
|
||||
)
|
||||
|
||||
type SpeechKitService struct {
|
||||
type speechKitConfig struct {
|
||||
ApiKey string
|
||||
FolderID string
|
||||
}
|
||||
|
||||
type speechKitService struct {
|
||||
sttConn *grpc.ClientConn
|
||||
opConn *grpc.ClientConn
|
||||
sttClient stt.AsyncRecognizerClient
|
||||
@@ -30,9 +34,9 @@ type SpeechKitService struct {
|
||||
folderID string
|
||||
}
|
||||
|
||||
func NewSpeechKitService() (*SpeechKitService, error) {
|
||||
apiKey := os.Getenv("YANDEX_CLOUD_API_KEY")
|
||||
folderID := os.Getenv("YANDEX_CLOUD_FOLDER_ID")
|
||||
func newSpeechKitService(cfg speechKitConfig) (*speechKitService, error) {
|
||||
apiKey := cfg.ApiKey
|
||||
folderID := cfg.FolderID
|
||||
|
||||
if apiKey == "" || folderID == "" {
|
||||
return nil, fmt.Errorf("missing required Yandex Cloud environment variables")
|
||||
@@ -55,7 +59,7 @@ func NewSpeechKitService() (*SpeechKitService, error) {
|
||||
sttClient := stt.NewAsyncRecognizerClient(sttConn)
|
||||
opClient := operation.NewOperationServiceClient(opConn)
|
||||
|
||||
return &SpeechKitService{
|
||||
return &speechKitService{
|
||||
sttConn: sttConn,
|
||||
opConn: opConn,
|
||||
sttClient: sttClient,
|
||||
@@ -65,7 +69,7 @@ func NewSpeechKitService() (*SpeechKitService, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SpeechKitService) Close() error {
|
||||
func (s *speechKitService) Close() error {
|
||||
var err1, err2 error
|
||||
if s.sttConn != nil {
|
||||
err1 = s.sttConn.Close()
|
||||
@@ -79,8 +83,8 @@ func (s *SpeechKitService) Close() error {
|
||||
return err2
|
||||
}
|
||||
|
||||
// RecognizeFileFromS3 запускает асинхронное распознавание файла из S3
|
||||
func (s *SpeechKitService) RecognizeFileFromS3(s3URI string) (string, error) {
|
||||
// recognizeFileFromS3 запускает асинхронное распознавание файла из S3
|
||||
func (s *speechKitService) recognizeFileFromS3(s3URI string) (string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Добавляем авторизацию и folder_id в контекст
|
||||
@@ -123,7 +127,7 @@ func (s *SpeechKitService) RecognizeFileFromS3(s3URI string) (string, error) {
|
||||
}
|
||||
|
||||
// GetRecognitionResult получает результат распознавания по ID операции
|
||||
func (s *SpeechKitService) GetRecognitionText(operationID string) (string, error) {
|
||||
func (s *speechKitService) getRecognitionText(operationID string) (string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Добавляем авторизацию и folder_id в контекст
|
||||
@@ -162,8 +166,8 @@ func (s *SpeechKitService) GetRecognitionText(operationID string) (string, error
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// CheckOperationStatus проверяет статус операции распознавания
|
||||
func (s *SpeechKitService) CheckOperationStatus(operationID string) (*operation.Operation, error) {
|
||||
// checkOperationStatus проверяет статус операции распознавания
|
||||
func (s *speechKitService) checkOperationStatus(operationID string) (*operation.Operation, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Api-Key "+s.apiKey)
|
@@ -1,8 +1,25 @@
|
||||
package contract
|
||||
|
||||
type ObjectStorage interface {
|
||||
import (
|
||||
"io"
|
||||
|
||||
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||
)
|
||||
|
||||
type AudioInfo struct {
|
||||
Seconds int // Длина аудиофайла в секундах
|
||||
}
|
||||
|
||||
type AudioMetaViewer interface {
|
||||
GetInfo(src string) (*AudioInfo, error)
|
||||
}
|
||||
|
||||
type AudioFileConverter interface {
|
||||
Convert(src, dest string) error
|
||||
}
|
||||
|
||||
type AudioRecognizer interface {
|
||||
Recognize(file io.Reader, fileName string) (operationID string, err error)
|
||||
GetRecognitionText(operationID string) (string, error)
|
||||
CheckRecognitionStatus(operationID string) (*entity.RecognitionResult, error)
|
||||
}
|
||||
|
@@ -10,3 +10,11 @@ type JobNotFoundError struct {
|
||||
func (e *JobNotFoundError) Error() string {
|
||||
return fmt.Sprintf("%s - %s", e.State, e.Message)
|
||||
}
|
||||
|
||||
type NoopJobError struct {
|
||||
State string
|
||||
}
|
||||
|
||||
func (e *NoopJobError) Error() string {
|
||||
return fmt.Sprintf("%s: no op job occur", e.State)
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -15,8 +16,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.vakhrushev.me/av/transcriber/internal/adapter/ffmpeg"
|
||||
"git.vakhrushev.me/av/transcriber/internal/adapter/sqlite"
|
||||
ffmpegconv "git.vakhrushev.me/av/transcriber/internal/adapter/converter/ffmpeg"
|
||||
ffmpegmv "git.vakhrushev.me/av/transcriber/internal/adapter/metaviewer/ffmpeg"
|
||||
"git.vakhrushev.me/av/transcriber/internal/adapter/recognizer"
|
||||
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/sqlite"
|
||||
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||
"git.vakhrushev.me/av/transcriber/internal/service"
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
@@ -57,9 +60,16 @@ func setupTestRouter(t *testing.T) (*gin.Engine, *TranscribeHandler) {
|
||||
fileRepo := sqlite.NewFileRepository(db, gq)
|
||||
jobRepo := sqlite.NewTranscriptJobRepository(db, gq)
|
||||
|
||||
converter := ffmpeg.NewFfmpegConverter()
|
||||
metaviewer := ffmpegmv.NewFfmpegMetaViewer()
|
||||
converter := ffmpegconv.NewFfmpegConverter()
|
||||
recognizer := &recognizer.MemoryAudioRecognizer{}
|
||||
|
||||
trsService := service.NewTranscribeService(jobRepo, fileRepo, converter)
|
||||
// Создаем тестовый логгер
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||
Level: slog.LevelError, // Только ошибки в тестах
|
||||
}))
|
||||
|
||||
trsService := service.NewTranscribeService(jobRepo, fileRepo, metaviewer, converter, recognizer, logger)
|
||||
|
||||
handler := NewTranscribeHandler(jobRepo, trsService)
|
||||
|
||||
|
@@ -2,10 +2,12 @@ package worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.vakhrushev.me/av/transcriber/internal/service"
|
||||
"git.vakhrushev.me/av/transcriber/internal/contract"
|
||||
"git.vakhrushev.me/av/transcriber/internal/metrics"
|
||||
)
|
||||
|
||||
// Worker представляет базовый интерфейс для всех воркеров
|
||||
@@ -14,121 +16,50 @@ type Worker interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
// ConversionWorker обрабатывает задачи конвертации
|
||||
type ConversionWorker struct {
|
||||
transcribeService *service.TranscribeService
|
||||
type CallbackWorker struct {
|
||||
name string
|
||||
f func() error
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewConversionWorker(transcribeService *service.TranscribeService) *ConversionWorker {
|
||||
return &ConversionWorker{
|
||||
transcribeService: transcribeService,
|
||||
func NewCallbackWorker(name string, f func() error, logger *slog.Logger) *CallbackWorker {
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
|
||||
return &CallbackWorker{
|
||||
name: name,
|
||||
f: f,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ConversionWorker) Name() string {
|
||||
return "ConversionWorker"
|
||||
func (w *CallbackWorker) Name() string {
|
||||
return w.name
|
||||
}
|
||||
|
||||
func (w *ConversionWorker) Start(ctx context.Context) {
|
||||
log.Printf("%s started", w.Name())
|
||||
func (w *CallbackWorker) Start(ctx context.Context) {
|
||||
w.logger.Info("Worker started", "worker_name", w.Name())
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("%s received shutdown signal", w.Name())
|
||||
w.logger.Info("Worker received shutdown signal", "worker_name", w.Name())
|
||||
return
|
||||
default:
|
||||
err := w.transcribeService.FindAndRunConversionJob()
|
||||
if err != nil {
|
||||
log.Printf("%s error: %v", w.Name(), err)
|
||||
err := w.f()
|
||||
_, isNoop := err.(*contract.NoopJobError)
|
||||
if !isNoop {
|
||||
metrics.WorkerJobCounter.WithLabelValues(w.Name(), strconv.FormatBool(err != nil)).Inc()
|
||||
}
|
||||
if err != nil && !isNoop {
|
||||
w.logger.Error("Worker error", "worker_name", w.Name(), "error", err)
|
||||
}
|
||||
|
||||
// Ждем 1 секунду перед следующей итерацией
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("%s received shutdown signal during sleep", w.Name())
|
||||
return
|
||||
case <-time.After(1 * time.Second):
|
||||
// Продолжаем работу
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TranscribeWorker обрабатывает задачи транскрипции
|
||||
type TranscribeWorker struct {
|
||||
transcribeService *service.TranscribeService
|
||||
}
|
||||
|
||||
func NewTranscribeWorker(transcribeService *service.TranscribeService) *TranscribeWorker {
|
||||
return &TranscribeWorker{
|
||||
transcribeService: transcribeService,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *TranscribeWorker) Name() string {
|
||||
return "TranscribeWorker"
|
||||
}
|
||||
|
||||
func (w *TranscribeWorker) Start(ctx context.Context) {
|
||||
log.Printf("%s started", w.Name())
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("%s received shutdown signal", w.Name())
|
||||
return
|
||||
default:
|
||||
err := w.transcribeService.FindAndRunTranscribeJob()
|
||||
if err != nil {
|
||||
log.Printf("%s error: %v", w.Name(), err)
|
||||
}
|
||||
|
||||
// Ждем 1 секунду перед следующей итерацией
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("%s received shutdown signal during sleep", w.Name())
|
||||
return
|
||||
case <-time.After(1 * time.Second):
|
||||
// Продолжаем работу
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckWorker обрабатывает задачи проверки статуса распознавания
|
||||
type CheckWorker struct {
|
||||
transcribeService *service.TranscribeService
|
||||
}
|
||||
|
||||
func NewCheckWorker(transcribeService *service.TranscribeService) *CheckWorker {
|
||||
return &CheckWorker{
|
||||
transcribeService: transcribeService,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *CheckWorker) Name() string {
|
||||
return "CheckWorker"
|
||||
}
|
||||
|
||||
func (w *CheckWorker) Start(ctx context.Context) {
|
||||
log.Printf("%s started", w.Name())
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("%s received shutdown signal", w.Name())
|
||||
return
|
||||
default:
|
||||
err := w.transcribeService.FindAndRunTranscribeCheckJob()
|
||||
if err != nil {
|
||||
log.Printf("%s error: %v", w.Name(), err)
|
||||
}
|
||||
|
||||
// Ждем 1 секунду перед следующей итерацией
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("%s received shutdown signal during sleep", w.Name())
|
||||
w.logger.Info("Worker received shutdown signal during sleep", "worker_name", w.Name())
|
||||
return
|
||||
case <-time.After(1 * time.Second):
|
||||
// Продолжаем работу
|
||||
|
78
internal/entity/recognition.go
Normal file
78
internal/entity/recognition.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package entity
|
||||
|
||||
// RecognitionStatus представляет статус операции транскрипции
|
||||
type RecognitionStatus int
|
||||
|
||||
const (
|
||||
// RecognitionStatusInProgress - операция в процессе выполнения
|
||||
RecognitionStatusInProgress RecognitionStatus = iota
|
||||
// RecognitionStatusCompleted - операция завершена успешно
|
||||
RecognitionStatusCompleted
|
||||
// RecognitionStatusFailed - операция завершена с ошибкой
|
||||
RecognitionStatusFailed
|
||||
)
|
||||
|
||||
// String возвращает строковое представление статуса
|
||||
func (s RecognitionStatus) String() string {
|
||||
switch s {
|
||||
case RecognitionStatusInProgress:
|
||||
return "in_progress"
|
||||
case RecognitionStatusCompleted:
|
||||
return "completed"
|
||||
case RecognitionStatusFailed:
|
||||
return "failed"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// RecognitionResult представляет результат операции транскрипции
|
||||
type RecognitionResult struct {
|
||||
Status RecognitionStatus
|
||||
Error string // Текст ошибки (заполняется при StatusFailed)
|
||||
}
|
||||
|
||||
// NewInProgressResult создает результат для операции в процессе выполнения
|
||||
func NewInProgressResult() *RecognitionResult {
|
||||
return &RecognitionResult{
|
||||
Status: RecognitionStatusInProgress,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCompletedResult создает результат для успешно завершенной операции
|
||||
func NewCompletedResult() *RecognitionResult {
|
||||
return &RecognitionResult{
|
||||
Status: RecognitionStatusCompleted,
|
||||
}
|
||||
}
|
||||
|
||||
// NewFailedResult создает результат для операции, завершенной с ошибкой
|
||||
func NewFailedResult(errorText string) *RecognitionResult {
|
||||
return &RecognitionResult{
|
||||
Status: RecognitionStatusFailed,
|
||||
Error: errorText,
|
||||
}
|
||||
}
|
||||
|
||||
// IsInProgress проверяет, находится ли операция в процессе выполнения
|
||||
func (r *RecognitionResult) IsInProgress() bool {
|
||||
return r.Status == RecognitionStatusInProgress
|
||||
}
|
||||
|
||||
// IsCompleted проверяет, завершена ли операция успешно
|
||||
func (r *RecognitionResult) IsCompleted() bool {
|
||||
return r.Status == RecognitionStatusCompleted
|
||||
}
|
||||
|
||||
// IsFailed проверяет, завершена ли операция с ошибкой
|
||||
func (r *RecognitionResult) IsFailed() bool {
|
||||
return r.Status == RecognitionStatusFailed
|
||||
}
|
||||
|
||||
// GetError возвращает текст ошибки (только для операций, завершенных с ошибкой)
|
||||
func (r *RecognitionResult) GetError() string {
|
||||
if r.IsFailed() {
|
||||
return r.Error
|
||||
}
|
||||
return ""
|
||||
}
|
56
internal/metrics/metrics.go
Normal file
56
internal/metrics/metrics.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
WorkerJobCounter = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "transcriber_worker_job_count",
|
||||
Help: "Count of jobs handled by each worker",
|
||||
},
|
||||
[]string{"name", "error"},
|
||||
)
|
||||
|
||||
// Размер принятых на обработку файлов (в байтах)
|
||||
InputFileSizeHistogram = promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "transcriber_input_file_size_bytes",
|
||||
Help: "Size of input files received for processing",
|
||||
Buckets: []float64{1024, 10240, 102400, 1048576, 10485760, 104857600, 1073741824}, // 1KB, 10KB, 100KB, 1MB, 10MB, 100MB, 1GB
|
||||
},
|
||||
[]string{"file_extension"},
|
||||
)
|
||||
|
||||
// Время конвертации файлов (в секундах)
|
||||
InputFileDurationHistogram = promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "transcriber_input_file_duration_seconds",
|
||||
Help: "Duration of input audio file",
|
||||
Buckets: []float64{15, 30, 60, 120, 300, 600, 1200, 1800, 2400, 3000, 3600, 7200, 10800, 14400},
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
|
||||
// Время конвертации файлов (в секундах)
|
||||
ConversionDurationHistogram = promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "transcriber_conversion_duration_seconds",
|
||||
Help: "Time taken to convert audio files",
|
||||
Buckets: []float64{0.1, 0.5, 1, 5, 10, 30, 60, 120, 300}, // 0.1s, 0.5s, 1s, 5s, 10s, 30s, 1m, 2m, 5m
|
||||
},
|
||||
[]string{"source_format", "target_format", "error"},
|
||||
)
|
||||
|
||||
// Размер файла после конвертации (в байтах)
|
||||
OutputFileSizeHistogram = promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "transcriber_output_file_size_bytes",
|
||||
Help: "Size of files after conversion",
|
||||
Buckets: []float64{1024, 10240, 102400, 1048576, 10485760, 104857600, 1073741824}, // 1KB, 10KB, 100KB, 1MB, 10MB, 100MB, 1GB
|
||||
},
|
||||
[]string{"format"},
|
||||
)
|
||||
)
|
@@ -1,87 +0,0 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
)
|
||||
|
||||
type S3Service struct {
|
||||
client *s3.Client
|
||||
uploader *manager.Uploader
|
||||
bucketName string
|
||||
}
|
||||
|
||||
func NewS3Service() (*S3Service, error) {
|
||||
region := os.Getenv("AWS_REGION")
|
||||
accessKey := os.Getenv("AWS_ACCESS_KEY_ID")
|
||||
secretKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
|
||||
bucketName := os.Getenv("S3_BUCKET_NAME")
|
||||
endpoint := os.Getenv("S3_ENDPOINT")
|
||||
|
||||
if region == "" || accessKey == "" || secretKey == "" || bucketName == "" {
|
||||
return nil, fmt.Errorf("missing required S3 environment variables")
|
||||
}
|
||||
|
||||
// Создаем конфигурацию
|
||||
cfg, err := config.LoadDefaultConfig(context.TODO(),
|
||||
config.WithRegion(region),
|
||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load AWS config: %w", err)
|
||||
}
|
||||
|
||||
// Создаем клиент S3
|
||||
var client *s3.Client
|
||||
if endpoint != "" {
|
||||
// Кастомный endpoint (например, для MinIO)
|
||||
client = s3.NewFromConfig(cfg, func(o *s3.Options) {
|
||||
o.BaseEndpoint = aws.String(endpoint)
|
||||
o.UsePathStyle = true
|
||||
})
|
||||
} else {
|
||||
// Стандартный AWS S3
|
||||
client = s3.NewFromConfig(cfg)
|
||||
}
|
||||
|
||||
uploader := manager.NewUploader(client)
|
||||
|
||||
return &S3Service{
|
||||
client: client,
|
||||
uploader: uploader,
|
||||
bucketName: bucketName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *S3Service) UploadFile(filePath, fileName string) error {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file %s: %w", filePath, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = s.uploader.Upload(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(s.bucketName),
|
||||
Key: aws.String(fileName),
|
||||
Body: file,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload file to S3: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *S3Service) FileUrl(fileName string) string {
|
||||
endpoint := strings.TrimRight(os.Getenv("S3_ENDPOINT"), "/")
|
||||
bucketName := os.Getenv("S3_BUCKET_NAME")
|
||||
return fmt.Sprintf("%s/%s/%s", endpoint, bucketName, fileName)
|
||||
}
|
@@ -3,31 +3,49 @@ package service
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.vakhrushev.me/av/transcriber/internal/contract"
|
||||
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||
"git.vakhrushev.me/av/transcriber/internal/service/s3"
|
||||
"git.vakhrushev.me/av/transcriber/internal/service/speechkit"
|
||||
"git.vakhrushev.me/av/transcriber/internal/metrics"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const baseStorageDir = "data/files"
|
||||
const (
|
||||
baseStorageDir = "data/files"
|
||||
|
||||
defaultAudioExt = "audio"
|
||||
)
|
||||
|
||||
type TranscribeService struct {
|
||||
jobRepo contract.TranscriptJobRepository
|
||||
fileRepo contract.FileRepository
|
||||
converter contract.AudioFileConverter
|
||||
jobRepo contract.TranscriptJobRepository
|
||||
fileRepo contract.FileRepository
|
||||
metaviewer contract.AudioMetaViewer
|
||||
converter contract.AudioFileConverter
|
||||
recognizer contract.AudioRecognizer
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewTranscribeService(jobRepo contract.TranscriptJobRepository, fileRepo contract.FileRepository, converter contract.AudioFileConverter) *TranscribeService {
|
||||
func NewTranscribeService(
|
||||
jobRepo contract.TranscriptJobRepository,
|
||||
fileRepo contract.FileRepository,
|
||||
metaviewer contract.AudioMetaViewer,
|
||||
converter contract.AudioFileConverter,
|
||||
recognizer contract.AudioRecognizer,
|
||||
logger *slog.Logger,
|
||||
) *TranscribeService {
|
||||
return &TranscribeService{
|
||||
jobRepo: jobRepo,
|
||||
fileRepo: fileRepo,
|
||||
converter: converter,
|
||||
jobRepo: jobRepo,
|
||||
fileRepo: fileRepo,
|
||||
metaviewer: metaviewer,
|
||||
converter: converter,
|
||||
recognizer: recognizer,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,16 +56,22 @@ func (s *TranscribeService) CreateTranscribeJob(file io.Reader, fileName string)
|
||||
// Определяем расширение файла
|
||||
ext := filepath.Ext(fileName)
|
||||
if ext == "" {
|
||||
ext = ".audio" // fallback если расширение не определено
|
||||
ext = fmt.Sprintf(".%s", defaultAudioExt) // fallback если расширение не определено
|
||||
}
|
||||
|
||||
// Создаем путь для сохранения файла
|
||||
storageFileName := fmt.Sprintf("%s%s", fileId, ext)
|
||||
storageFilePath := filepath.Join(baseStorageDir, storageFileName)
|
||||
|
||||
s.logger.Info("Creating transcribe job",
|
||||
"file_id", fileId,
|
||||
"file_name", fileName,
|
||||
"storage_path", storageFilePath)
|
||||
|
||||
// Создаем файл на диске
|
||||
dst, err := os.Create(storageFilePath)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to create file", "error", err, "path", storageFilePath)
|
||||
return nil, err
|
||||
}
|
||||
defer dst.Close()
|
||||
@@ -55,9 +79,29 @@ func (s *TranscribeService) CreateTranscribeJob(file io.Reader, fileName string)
|
||||
// Копируем содержимое загруженного файла
|
||||
size, err := io.Copy(dst, file)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to copy file content", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := dst.Close(); err != nil {
|
||||
s.logger.Error("Failed to close file", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info, err := s.metaviewer.GetInfo(storageFilePath)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get file info", "error", err, "path", storageFilePath)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("File uploaded successfully",
|
||||
"file_id", fileId,
|
||||
"size", size,
|
||||
"duration_seconds", info.Seconds)
|
||||
|
||||
metrics.InputFileDurationHistogram.WithLabelValues().Observe(float64(info.Seconds))
|
||||
metrics.InputFileSizeHistogram.WithLabelValues(ext).Observe(float64(size))
|
||||
|
||||
// Создаем запись в таблице files
|
||||
fileRecord := &entity.File{
|
||||
Id: fileId,
|
||||
@@ -70,6 +114,7 @@ func (s *TranscribeService) CreateTranscribeJob(file io.Reader, fileName string)
|
||||
if err := s.fileRepo.Create(fileRecord); err != nil {
|
||||
// Удаляем файл если не удалось создать запись в БД
|
||||
os.Remove(storageFilePath)
|
||||
s.logger.Error("Failed to create file record", "error", err, "file_id", fileId)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -87,9 +132,11 @@ func (s *TranscribeService) CreateTranscribeJob(file io.Reader, fileName string)
|
||||
}
|
||||
|
||||
if err := s.jobRepo.Create(job); err != nil {
|
||||
s.logger.Error("Failed to create job record", "error", err, "job_id", jobId)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Transcribe job created successfully", "job_id", jobId, "file_id", fileId)
|
||||
return job, nil
|
||||
}
|
||||
|
||||
@@ -100,13 +147,17 @@ func (s *TranscribeService) FindAndRunConversionJob() error {
|
||||
job, err := s.jobRepo.FindAndAcquire(entity.StateCreated, acquisitionId, rottingTime)
|
||||
if err != nil {
|
||||
if _, ok := err.(*contract.JobNotFoundError); ok {
|
||||
return nil
|
||||
return &contract.NoopJobError{State: entity.StateCreated}
|
||||
}
|
||||
s.logger.Error("Failed to find and acquire conversion job", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Starting conversion job", "job_id", job.Id, "acquisition_id", acquisitionId)
|
||||
|
||||
srcFile, err := s.fileRepo.GetByID(*job.FileID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get source file", "error", err, "file_id", *job.FileID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -116,16 +167,50 @@ func (s *TranscribeService) FindAndRunConversionJob() error {
|
||||
destFileName := fmt.Sprintf("%s%s", destFileId, ".ogg")
|
||||
destFilePath := filepath.Join(baseStorageDir, destFileName)
|
||||
|
||||
// Получаем расширение исходного файла для метрики
|
||||
srcExt := strings.TrimPrefix(filepath.Ext(srcFile.FileName), ".")
|
||||
if srcExt == "" {
|
||||
srcExt = defaultAudioExt
|
||||
}
|
||||
|
||||
s.logger.Info("Converting file",
|
||||
"job_id", job.Id,
|
||||
"src_path", srcFilePath,
|
||||
"dest_path", destFilePath,
|
||||
"src_format", srcExt)
|
||||
|
||||
// Измеряем время конвертации
|
||||
startTime := time.Now()
|
||||
err = s.converter.Convert(srcFilePath, destFilePath)
|
||||
conversionDuration := time.Since(startTime)
|
||||
|
||||
// Записываем метрику времени конвертации
|
||||
metrics.ConversionDurationHistogram.
|
||||
WithLabelValues(srcExt, "ogg", strconv.FormatBool(err != nil)).
|
||||
Observe(conversionDuration.Seconds())
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error("File conversion failed",
|
||||
"error", err,
|
||||
"job_id", job.Id,
|
||||
"duration", conversionDuration)
|
||||
return err
|
||||
}
|
||||
|
||||
stat, err := os.Stat(destFilePath)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to stat converted file", "error", err, "path", destFilePath)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("File conversion completed",
|
||||
"job_id", job.Id,
|
||||
"duration", conversionDuration,
|
||||
"output_size", stat.Size())
|
||||
|
||||
// Записываем метрику размера выходного файла
|
||||
metrics.OutputFileSizeHistogram.WithLabelValues("ogg").Observe(float64(stat.Size()))
|
||||
|
||||
// Создаем запись в таблице files
|
||||
destFileRecord := &entity.File{
|
||||
Id: destFileId,
|
||||
@@ -140,14 +225,17 @@ func (s *TranscribeService) FindAndRunConversionJob() error {
|
||||
|
||||
err = s.fileRepo.Create(destFileRecord)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to create converted file record", "error", err, "file_id", destFileId)
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.jobRepo.Save(job)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to save job", "error", err, "job_id", job.Id)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Conversion job completed successfully", "job_id", job.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -158,48 +246,45 @@ func (s *TranscribeService) FindAndRunTranscribeJob() error {
|
||||
jobRecord, err := s.jobRepo.FindAndAcquire(entity.StateConverted, acquisitionId, rottingTime)
|
||||
if err != nil {
|
||||
if _, ok := err.(*contract.JobNotFoundError); ok {
|
||||
return nil
|
||||
return &contract.NoopJobError{State: entity.StateConverted}
|
||||
}
|
||||
s.logger.Error("Failed to find and acquire transcribe job", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Starting transcribe job", "job_id", jobRecord.Id, "acquisition_id", acquisitionId)
|
||||
|
||||
fileRecord, err := s.fileRepo.GetByID(*jobRecord.FileID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get file record", "error", err, "file_id", *jobRecord.FileID)
|
||||
return err
|
||||
}
|
||||
|
||||
filePath := filepath.Join(baseStorageDir, fileRecord.FileName)
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to open file", "error", err, "path", filePath)
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
destFileId := uuid.NewString()
|
||||
destFileRecord := fileRecord.CopyWithStorage(destFileId, entity.StorageS3)
|
||||
|
||||
// Создаем S3 сервис
|
||||
s3Service, err := s3.NewS3Service()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Загружаем файл на S3
|
||||
err = s3Service.UploadFile(filePath, destFileRecord.FileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Создаем SpeechKit сервис
|
||||
speechKitService, err := speechkit.NewSpeechKitService()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Формируем S3 URI для файла
|
||||
s3URI := s3Service.FileUrl(destFileRecord.FileName)
|
||||
s.logger.Info("Starting recognition", "job_id", jobRecord.Id, "file_path", filePath)
|
||||
|
||||
// Запускаем асинхронное распознавание
|
||||
operationID, err := speechKitService.RecognizeFileFromS3(s3URI)
|
||||
operationID, err := s.recognizer.Recognize(file, destFileRecord.FileName)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to start recognition", "error", err, "job_id", jobRecord.Id)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Recognition started",
|
||||
"job_id", jobRecord.Id,
|
||||
"operation_id", operationID)
|
||||
|
||||
// Обновляем задачу с ID операции распознавания
|
||||
jobRecord.FileID = &destFileId
|
||||
jobRecord.RecognitionOpID = &operationID
|
||||
@@ -208,14 +293,17 @@ func (s *TranscribeService) FindAndRunTranscribeJob() error {
|
||||
|
||||
err = s.fileRepo.Create(destFileRecord)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to create S3 file record", "error", err, "file_id", destFileId)
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.jobRepo.Save(jobRecord)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to save job", "error", err, "job_id", jobRecord.Id)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Transcribe job updated successfully", "job_id", jobRecord.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -226,69 +314,76 @@ func (s *TranscribeService) FindAndRunTranscribeCheckJob() error {
|
||||
job, err := s.jobRepo.FindAndAcquire(entity.StateTranscribe, acquisitionId, rottingTime)
|
||||
if err != nil {
|
||||
if _, ok := err.(*contract.JobNotFoundError); ok {
|
||||
return nil
|
||||
return &contract.NoopJobError{State: entity.StateTranscribe}
|
||||
}
|
||||
return err
|
||||
s.logger.Error("Failed to find and acquire transcribe check job", "error", err)
|
||||
return fmt.Errorf("failed find and acquire job: %s, %w", entity.StateTranscribe, err)
|
||||
}
|
||||
|
||||
if job.RecognitionOpID == nil {
|
||||
s.logger.Error("Recognition operation ID not found", "job_id", job.Id)
|
||||
return fmt.Errorf("recogniton opId not found for job: %s", job.Id)
|
||||
}
|
||||
|
||||
// Создаем SpeechKit сервис
|
||||
speechKitService, err := speechkit.NewSpeechKitService()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer speechKitService.Close()
|
||||
|
||||
opId := *job.RecognitionOpID
|
||||
|
||||
// Проверяем статус операции
|
||||
log.Printf("Check operation status: id %s\n", opId)
|
||||
operation, err := speechKitService.CheckOperationStatus(opId)
|
||||
s.logger.Info("Checking operation status", "job_id", job.Id, "operation_id", opId)
|
||||
recResult, err := s.recognizer.CheckRecognitionStatus(opId)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to check recognition status", "error", err, "operation_id", opId)
|
||||
return err
|
||||
}
|
||||
|
||||
if !operation.Done {
|
||||
if recResult.IsInProgress() {
|
||||
// Операция еще не завершена, оставляем в статусе обработки
|
||||
log.Printf("Operation in progress: id %s\n", opId)
|
||||
s.logger.Info("Operation in progress", "job_id", job.Id, "operation_id", opId)
|
||||
delayTime := time.Now().Add(10 * time.Second)
|
||||
job.MoveToStateAndDelay(entity.StateTranscribe, &delayTime)
|
||||
err := s.jobRepo.Save(job)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to save job", "error", err, "job_id", job.Id)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if opErr := operation.GetError(); opErr != nil {
|
||||
errorText := fmt.Sprintf("operation failed: code %d, message: %s", opErr.Code, opErr.Message)
|
||||
log.Printf("Operation failed: id %s, message %s\n", opId, errorText)
|
||||
if recResult.IsFailed() {
|
||||
errorText := recResult.GetError()
|
||||
s.logger.Error("Operation failed",
|
||||
"job_id", job.Id,
|
||||
"operation_id", opId,
|
||||
"error_message", errorText)
|
||||
job.Fail(errorText)
|
||||
err := s.jobRepo.Save(job)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to save failed job", "error", err, "job_id", job.Id)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Операция завершена, получаем результат
|
||||
transcriptionText, err := speechKitService.GetRecognitionText(*job.RecognitionOpID)
|
||||
transcriptionText, err := s.recognizer.GetRecognitionText(opId)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get recognition text", "error", err, "operation_id", opId)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Operation done: id %s\n", opId)
|
||||
s.logger.Info("Operation completed successfully",
|
||||
"job_id", job.Id,
|
||||
"operation_id", opId,
|
||||
"text_length", len(transcriptionText))
|
||||
|
||||
// Обновляем задачу с результатом
|
||||
job.Done(transcriptionText)
|
||||
|
||||
err = s.jobRepo.Save(job)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to save completed job", "error", err, "job_id", job.Id)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Transcribe check job completed successfully", "job_id", job.Id)
|
||||
return nil
|
||||
}
|
||||
|
11
lefthook.yml
Normal file
11
lefthook.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
# Refer for explanation to following link:
|
||||
# https://lefthook.dev/configuration/
|
||||
|
||||
templates:
|
||||
av-hooks-dir: "/home/av/projects/private/git-hooks"
|
||||
|
||||
pre-commit:
|
||||
jobs:
|
||||
|
||||
- name: "gitleaks"
|
||||
run: "gitleaks git --staged"
|
101
main.go
101
main.go
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -12,8 +12,10 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.vakhrushev.me/av/transcriber/internal/adapter/ffmpeg"
|
||||
"git.vakhrushev.me/av/transcriber/internal/adapter/sqlite"
|
||||
ffmpegconv "git.vakhrushev.me/av/transcriber/internal/adapter/converter/ffmpeg"
|
||||
ffmpegmv "git.vakhrushev.me/av/transcriber/internal/adapter/metaviewer/ffmpeg"
|
||||
"git.vakhrushev.me/av/transcriber/internal/adapter/recognizer/yandex"
|
||||
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/sqlite"
|
||||
httpcontroller "git.vakhrushev.me/av/transcriber/internal/controller/http"
|
||||
"git.vakhrushev.me/av/transcriber/internal/controller/worker"
|
||||
"git.vakhrushev.me/av/transcriber/internal/service"
|
||||
@@ -23,47 +25,78 @@ import (
|
||||
"github.com/joho/godotenv"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/pressly/goose/v3"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
sloggin "github.com/samber/slog-gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Создаем структурированный логгер
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
slog.SetDefault(logger)
|
||||
|
||||
// Загружаем переменные окружения из .env файла
|
||||
if err := godotenv.Load(); err != nil {
|
||||
log.Println("Warning: .env file not found, using system environment variables")
|
||||
logger.Warn("Warning: .env file not found, using system environment variables")
|
||||
}
|
||||
|
||||
// Создаем директории если они не существуют
|
||||
if err := os.MkdirAll("data/files", 0755); err != nil {
|
||||
log.Fatal("Failed to create data/files directory:", err)
|
||||
logger.Error("Failed to create data/files directory", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite3", "data/transcriber.db")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open database: %v", err)
|
||||
logger.Error("failed to open database", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
log.Fatalf("failed to ping database: %v", err)
|
||||
logger.Error("failed to ping database", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
gq := goqu.New("sqlite3", db)
|
||||
|
||||
// Запускаем миграции
|
||||
if err := RunMigrations(db, "migrations"); err != nil {
|
||||
log.Fatal("Failed to run migrations:", err)
|
||||
if err := RunMigrations(db, "migrations", logger); err != nil {
|
||||
logger.Error("Failed to run migrations", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Создаем репозитории
|
||||
fileRepo := sqlite.NewFileRepository(db, gq)
|
||||
jobRepo := sqlite.NewTranscriptJobRepository(db, gq)
|
||||
|
||||
converter := ffmpeg.NewFfmpegConverter()
|
||||
// Создаем адаптеры
|
||||
metaviewer := ffmpegmv.NewFfmpegMetaViewer()
|
||||
converter := ffmpegconv.NewFfmpegConverter()
|
||||
|
||||
transcribeService := service.NewTranscribeService(jobRepo, fileRepo, converter)
|
||||
recognizer, err := yandex.NewYandexAudioRecognizerService(yandex.YandexAudioRecognizerConfig{
|
||||
Region: os.Getenv("AWS_REGION"),
|
||||
AccessKey: os.Getenv("AWS_ACCESS_KEY_ID"),
|
||||
SecretKey: os.Getenv("AWS_SECRET_ACCESS_KEY"),
|
||||
BucketName: os.Getenv("S3_BUCKET_NAME"),
|
||||
Endpoint: os.Getenv("S3_ENDPOINT"),
|
||||
ApiKey: os.Getenv("YANDEX_CLOUD_API_KEY"),
|
||||
FolderID: os.Getenv("YANDEX_CLOUD_FOLDER_ID"),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error("failed to create audio recognizer", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer recognizer.Close()
|
||||
|
||||
// Создаем сервисы
|
||||
transcribeService := service.NewTranscribeService(jobRepo, fileRepo, metaviewer, converter, recognizer, logger)
|
||||
|
||||
// Создаем воркеры
|
||||
conversionWorker := worker.NewConversionWorker(transcribeService)
|
||||
transcribeWorker := worker.NewTranscribeWorker(transcribeService)
|
||||
checkWorker := worker.NewCheckWorker(transcribeService)
|
||||
conversionWorker := worker.NewCallbackWorker("conversion_worker", transcribeService.FindAndRunConversionJob, logger)
|
||||
transcribeWorker := worker.NewCallbackWorker("transcribe_worker", transcribeService.FindAndRunTranscribeJob, logger)
|
||||
checkWorker := worker.NewCallbackWorker("check_worker", transcribeService.FindAndRunTranscribeCheckJob, logger)
|
||||
|
||||
workers := []worker.Worker{
|
||||
conversionWorker,
|
||||
@@ -84,13 +117,18 @@ func main() {
|
||||
go func(worker worker.Worker) {
|
||||
defer wg.Done()
|
||||
worker.Start(ctx)
|
||||
log.Printf("%s stopped", worker.Name())
|
||||
logger.Info("Worker stopped", "worker_name", worker.Name())
|
||||
}(w)
|
||||
}
|
||||
|
||||
// Создаем Gin middleware для логирования
|
||||
gin.SetMode(gin.DebugMode)
|
||||
router := gin.New()
|
||||
router.Use(sloggin.New(logger))
|
||||
router.Use(gin.Recovery())
|
||||
|
||||
// Запускаем HTTP сервер для API (создание задач и проверка статуса)
|
||||
transcribeHandler := httpcontroller.NewTranscribeHandler(jobRepo, transcribeService)
|
||||
router := gin.Default()
|
||||
|
||||
// Настраиваем роуты только для создания задач и проверки статуса
|
||||
api := router.Group("/api")
|
||||
@@ -110,6 +148,9 @@ func main() {
|
||||
})
|
||||
})
|
||||
|
||||
// Добавляем эндпоинт для метрик Prometheus
|
||||
router.GET("/metrics", gin.WrapH(promhttp.Handler()))
|
||||
|
||||
// Создаем HTTP сервер
|
||||
srv := &http.Server{
|
||||
Addr: ":8080",
|
||||
@@ -120,9 +161,9 @@ func main() {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.Println("Starting HTTP server on :8080")
|
||||
logger.Info("Starting HTTP server", "port", 8080)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("HTTP server error: %v", err)
|
||||
logger.Error("HTTP server error", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -130,24 +171,24 @@ func main() {
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
log.Println("Transcriber service started with background workers")
|
||||
log.Println("Workers: ConversionWorker, TranscribeWorker, CheckWorker")
|
||||
log.Println("Press Ctrl+C to stop...")
|
||||
logger.Info("Transcriber service started with background workers")
|
||||
logger.Info("Workers: ConversionWorker, TranscribeWorker, CheckWorker")
|
||||
logger.Info("Press Ctrl+C to stop...")
|
||||
|
||||
// Ждем сигнал завершения
|
||||
<-sigChan
|
||||
log.Println("Received shutdown signal, initiating graceful shutdown...")
|
||||
logger.Info("Received shutdown signal, initiating graceful shutdown...")
|
||||
|
||||
// Создаем контекст с таймаутом для graceful shutdown HTTP сервера
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer shutdownCancel()
|
||||
|
||||
// Останавливаем HTTP сервер
|
||||
log.Println("Shutting down HTTP server...")
|
||||
logger.Info("Shutting down HTTP server...")
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
log.Printf("HTTP server forced to shutdown: %v", err)
|
||||
logger.Error("HTTP server forced to shutdown", "error", err)
|
||||
} else {
|
||||
log.Println("HTTP server stopped gracefully")
|
||||
logger.Info("HTTP server stopped gracefully")
|
||||
}
|
||||
|
||||
// Отменяем контекст для остановки воркеров
|
||||
@@ -163,15 +204,15 @@ func main() {
|
||||
// Ждем завершения всех воркеров или таймаута в 10 секунд
|
||||
select {
|
||||
case <-done:
|
||||
log.Println("All workers stopped gracefully")
|
||||
logger.Info("All workers stopped gracefully")
|
||||
case <-time.After(10 * time.Second):
|
||||
log.Println("Timeout reached, forcing shutdown")
|
||||
logger.Warn("Timeout reached, forcing shutdown")
|
||||
}
|
||||
|
||||
log.Println("Transcriber service stopped")
|
||||
logger.Info("Transcriber service stopped")
|
||||
}
|
||||
|
||||
func RunMigrations(db *sql.DB, migrationsDir string) error {
|
||||
func RunMigrations(db *sql.DB, migrationsDir string, logger *slog.Logger) error {
|
||||
if err := goose.SetDialect("sqlite3"); err != nil {
|
||||
return fmt.Errorf("failed to set goose dialect: %w", err)
|
||||
}
|
||||
@@ -180,6 +221,6 @@ func RunMigrations(db *sql.DB, migrationsDir string) error {
|
||||
return fmt.Errorf("failed to run migrations: %w", err)
|
||||
}
|
||||
|
||||
log.Println("Migrations completed successfully")
|
||||
logger.Info("Migrations completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user