feat: refactor application to use Gin framework, update routing and middleware handling
This commit is contained in:
33
go.mod
33
go.mod
@@ -1,6 +1,8 @@
|
|||||||
module tts
|
module tts
|
||||||
|
|
||||||
go 1.22
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
@@ -9,11 +11,28 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/bytedance/sonic v1.13.1 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||||
|
github.com/gin-gonic/gin v1.10.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.25.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
@@ -21,11 +40,17 @@ require (
|
|||||||
github.com/spf13/cast v1.6.0 // indirect
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
go.uber.org/multierr v1.9.0 // indirect
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
|
golang.org/x/arch v0.15.0 // indirect
|
||||||
|
golang.org/x/crypto v0.36.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/net v0.37.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
|
golang.org/x/text v0.23.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
62
go.sum
62
go.sum
@@ -1,3 +1,12 @@
|
|||||||
|
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
|
||||||
|
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
|
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
|
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
@@ -6,22 +15,54 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
|||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||||
|
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||||
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
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.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
|
||||||
|
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
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/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
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/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
@@ -51,22 +92,41 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||||
|
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
||||||
|
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||||
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
|
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||||
|
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -75,3 +135,5 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"tts/internal/config"
|
"tts/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,13 +29,7 @@ func NewPagesHandler(templatesDir string, cfg *config.Config) (*PagesHandler, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HandleIndex 处理首页请求
|
// HandleIndex 处理首页请求
|
||||||
func (h *PagesHandler) HandleIndex(w http.ResponseWriter, r *http.Request) {
|
func (h *PagesHandler) HandleIndex(c *gin.Context) {
|
||||||
// 如果不是根路径,返回404
|
|
||||||
if r.URL.Path != "/" && r.URL.Path != "/index.html" {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 准备模板数据
|
// 准备模板数据
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"BasePath": h.config.Server.BasePath,
|
"BasePath": h.config.Server.BasePath,
|
||||||
@@ -45,17 +39,17 @@ func (h *PagesHandler) HandleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 设置内容类型
|
// 设置内容类型
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|
||||||
// 渲染模板
|
// 渲染模板
|
||||||
if err := h.templates.ExecuteTemplate(w, "index.html", data); err != nil {
|
if err := h.templates.ExecuteTemplate(c.Writer, "index.html", data); err != nil {
|
||||||
http.Error(w, "模板渲染失败: "+err.Error(), http.StatusInternalServerError)
|
c.AbortWithStatusJSON(500, gin.H{"error": "模板渲染失败: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleAPIDoc 处理API文档请求
|
// HandleAPIDoc 处理API文档请求
|
||||||
func (h *PagesHandler) HandleAPIDoc(w http.ResponseWriter, r *http.Request) {
|
func (h *PagesHandler) HandleAPIDoc(c *gin.Context) {
|
||||||
// 准备模板数据
|
// 准备模板数据
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"BasePath": h.config.Server.BasePath,
|
"BasePath": h.config.Server.BasePath,
|
||||||
@@ -66,11 +60,11 @@ func (h *PagesHandler) HandleAPIDoc(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 设置内容类型
|
// 设置内容类型
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|
||||||
// 渲染模板
|
// 渲染模板
|
||||||
if err := h.templates.ExecuteTemplate(w, "api-doc.html", data); err != nil {
|
if err := h.templates.ExecuteTemplate(c.Writer, "api-doc.html", data); err != nil {
|
||||||
http.Error(w, "模板渲染失败: "+err.Error(), http.StatusInternalServerError)
|
c.AbortWithStatusJSON(500, gin.H{"error": "模板渲染失败: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -15,8 +14,84 @@ import (
|
|||||||
"tts/internal/models"
|
"tts/internal/models"
|
||||||
"tts/internal/tts"
|
"tts/internal/tts"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// truncateForLog 截断文本用于日志显示,同时显示开头和结尾
|
||||||
|
func truncateForLog(text string, maxLength int) string {
|
||||||
|
// 先去除换行符
|
||||||
|
text = strings.ReplaceAll(text, "\n", " ")
|
||||||
|
text = strings.ReplaceAll(text, "\r", " ")
|
||||||
|
|
||||||
|
runes := []rune(text)
|
||||||
|
if len(runes) <= maxLength {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
// 计算开头和结尾各显示多少字符
|
||||||
|
halfLength := maxLength / 2
|
||||||
|
return string(runes[:halfLength]) + "..." + string(runes[len(runes)-halfLength:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// audioMerge 音频合并
|
||||||
|
func audioMerge(audioSegments [][]byte) ([]byte, error) {
|
||||||
|
if len(audioSegments) == 0 {
|
||||||
|
return nil, fmt.Errorf("没有音频片段可合并")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 ffmpeg 合并音频
|
||||||
|
tempDir, err := os.MkdirTemp("", "audio_merge_")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
listFile := filepath.Join(tempDir, "concat.txt")
|
||||||
|
lf, err := os.Create(listFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, seg := range audioSegments {
|
||||||
|
segFile := filepath.Join(tempDir, fmt.Sprintf("seg_%d.mp3", i))
|
||||||
|
if err := os.WriteFile(segFile, seg, 0644); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := lf.WriteString(fmt.Sprintf("file '%s'\n", segFile)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lf.Close()
|
||||||
|
|
||||||
|
outputFile := filepath.Join(tempDir, "output.mp3")
|
||||||
|
|
||||||
|
cmd := exec.Command("ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", listFile, "-c", "copy", outputFile)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedData, err := os.ReadFile(outputFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Printf("使用ffmpeg合并完成,总大小: %s", formatFileSize(len(mergedData)))
|
||||||
|
return mergedData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatFileSize 格式化文件大小
|
||||||
|
func formatFileSize(size int) string {
|
||||||
|
switch {
|
||||||
|
case size < 1024:
|
||||||
|
return fmt.Sprintf("%d B", size)
|
||||||
|
case size < 1024*1024:
|
||||||
|
return fmt.Sprintf("%.2f KB", float64(size)/1024.0)
|
||||||
|
case size < 1024*1024*1024:
|
||||||
|
return fmt.Sprintf("%.2f MB", float64(size)/(1024.0*1024.0))
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%.2f GB", float64(size)/(1024.0*1024.0*1024.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TTSHandler 处理TTS请求
|
// TTSHandler 处理TTS请求
|
||||||
type TTSHandler struct {
|
type TTSHandler struct {
|
||||||
ttsService tts.Service
|
ttsService tts.Service
|
||||||
@@ -32,13 +107,13 @@ func NewTTSHandler(service tts.Service, cfg *config.Config) *TTSHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HandleOpenAITTS 处理OpenAI兼容的TTS请求
|
// HandleOpenAITTS 处理OpenAI兼容的TTS请求
|
||||||
func (h *TTSHandler) HandleOpenAITTS(w http.ResponseWriter, r *http.Request) {
|
func (h *TTSHandler) HandleOpenAITTS(c *gin.Context) {
|
||||||
// 记录请求开始时间
|
// 记录请求开始时间
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
// 只支持POST请求
|
// 只支持POST请求
|
||||||
if r.Method != http.MethodPost {
|
if c.Request.Method != http.MethodPost {
|
||||||
http.Error(w, "仅支持POST请求", http.StatusMethodNotAllowed)
|
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"error": "仅支持POST请求"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +125,8 @@ func (h *TTSHandler) HandleOpenAITTS(w http.ResponseWriter, r *http.Request) {
|
|||||||
Speed float64 `json:"speed"`
|
Speed float64 `json:"speed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&openaiReq); err != nil {
|
if err := c.ShouldBindJSON(&openaiReq); err != nil {
|
||||||
http.Error(w, "无效的JSON请求: "+err.Error(), http.StatusBadRequest)
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "无效的JSON请求: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +135,7 @@ func (h *TTSHandler) HandleOpenAITTS(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// 检查必需字段
|
// 检查必需字段
|
||||||
if openaiReq.Input == "" {
|
if openaiReq.Input == "" {
|
||||||
http.Error(w, "input字段不能为空", http.StatusBadRequest)
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "input字段不能为空"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +173,7 @@ func (h *TTSHandler) HandleOpenAITTS(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// 检查文本长度
|
// 检查文本长度
|
||||||
if len(req.Text) > h.config.TTS.MaxTextLength {
|
if len(req.Text) > h.config.TTS.MaxTextLength {
|
||||||
http.Error(w, "文本长度超过限制", http.StatusBadRequest)
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "文本长度超过限制"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,25 +182,25 @@ func (h *TTSHandler) HandleOpenAITTS(w http.ResponseWriter, r *http.Request) {
|
|||||||
if len(req.Text) > segmentThreshold && len(req.Text) <= h.config.TTS.MaxTextLength {
|
if len(req.Text) > segmentThreshold && len(req.Text) <= h.config.TTS.MaxTextLength {
|
||||||
log.Printf("文本长度 %d 超过阈值 %d,使用分段处理", len(req.Text), segmentThreshold)
|
log.Printf("文本长度 %d 超过阈值 %d,使用分段处理", len(req.Text), segmentThreshold)
|
||||||
// 使用分段处理
|
// 使用分段处理
|
||||||
h.handleSegmentedTTS(w, r, req)
|
h.handleSegmentedTTS(c, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 非流式模式处理
|
// 非流式模式处理
|
||||||
synthStart := time.Now()
|
synthStart := time.Now()
|
||||||
resp, err := h.ttsService.SynthesizeSpeech(r.Context(), req)
|
resp, err := h.ttsService.SynthesizeSpeech(c.Request.Context(), req)
|
||||||
synthTime := time.Since(synthStart)
|
synthTime := time.Since(synthStart)
|
||||||
log.Printf("TTS合成耗时: %v, 文本长度: %d", synthTime, len(req.Text))
|
log.Printf("TTS合成耗时: %v, 文本长度: %d", synthTime, len(req.Text))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "语音合成失败: "+err.Error(), http.StatusInternalServerError)
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "语音合成失败: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置响应
|
// 设置响应
|
||||||
w.Header().Set("Content-Type", "audio/mpeg")
|
c.Header("Content-Type", "audio/mpeg")
|
||||||
writeStart := time.Now()
|
writeStart := time.Now()
|
||||||
w.Write(resp.AudioContent)
|
c.Writer.Write(resp.AudioContent)
|
||||||
writeTime := time.Since(writeStart)
|
writeTime := time.Since(writeStart)
|
||||||
|
|
||||||
// 记录总耗时
|
// 记录总耗时
|
||||||
@@ -135,61 +210,51 @@ func (h *TTSHandler) HandleOpenAITTS(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HandleTTS 处理TTS请求
|
// HandleTTS 处理TTS请求
|
||||||
func (h *TTSHandler) HandleTTS(w http.ResponseWriter, r *http.Request) {
|
func (h *TTSHandler) HandleTTS(c *gin.Context) {
|
||||||
// 记录请求开始时间
|
// 记录请求开始时间
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
// 解析请求参数
|
// 解析请求参数
|
||||||
var req models.TTSRequest
|
var req models.TTSRequest
|
||||||
|
|
||||||
switch r.Method {
|
switch c.Request.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
// 从URL参数获取
|
// 从URL参数获取
|
||||||
q := r.URL.Query()
|
|
||||||
req = models.TTSRequest{
|
req = models.TTSRequest{
|
||||||
Text: q.Get("t"),
|
Text: c.Query("t"),
|
||||||
Voice: q.Get("v"),
|
Voice: c.Query("v"),
|
||||||
Rate: q.Get("r"),
|
Rate: c.Query("r"),
|
||||||
Pitch: q.Get("p"),
|
Pitch: c.Query("p"),
|
||||||
Style: q.Get("s"),
|
Style: c.Query("s"),
|
||||||
}
|
}
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
// 从POST JSON体获取
|
// 从POST JSON体获取
|
||||||
if r.Header.Get("Content-Type") == "application/json" {
|
if c.ContentType() == "application/json" {
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
log.Printf("JSON解析错误: %v", err)
|
log.Printf("JSON解析错误: %v", err)
|
||||||
http.Error(w, "无效的JSON请求", http.StatusBadRequest)
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "无效的JSON请求"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 表单数据
|
// 表单数据
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := c.ShouldBind(&req); err != nil {
|
||||||
log.Printf("表单解析错误: %v", err)
|
log.Printf("表单解析错误: %v", err)
|
||||||
http.Error(w, "无法解析表单数据", http.StatusBadRequest)
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "无法解析表单数据"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req = models.TTSRequest{
|
|
||||||
Text: r.FormValue("text"),
|
|
||||||
Voice: r.FormValue("voice"),
|
|
||||||
Rate: r.FormValue("rate"),
|
|
||||||
Pitch: r.FormValue("pitch"),
|
|
||||||
Style: r.FormValue("style"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
log.Printf("不支持的HTTP方法: %s", r.Method)
|
log.Printf("不支持的HTTP方法: %s", c.Request.Method)
|
||||||
http.Error(w, "仅支持GET和POST请求", http.StatusMethodNotAllowed)
|
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"error": "仅支持GET和POST请求"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录参数解析耗时
|
|
||||||
parseTime := time.Since(startTime)
|
parseTime := time.Since(startTime)
|
||||||
log.Printf("请求参数解析耗时: %v", parseTime)
|
|
||||||
|
|
||||||
// 验证必要参数
|
// 验证必要参数
|
||||||
if req.Text == "" {
|
if req.Text == "" {
|
||||||
log.Print("错误: 未提供文本参数")
|
log.Print("错误: 未提供文本参数")
|
||||||
http.Error(w, "必须提供文本参数", http.StatusBadRequest)
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "必须提供文本参数"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +271,7 @@ func (h *TTSHandler) HandleTTS(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// 检查文本长度
|
// 检查文本长度
|
||||||
if len(req.Text) > h.config.TTS.MaxTextLength {
|
if len(req.Text) > h.config.TTS.MaxTextLength {
|
||||||
http.Error(w, "文本长度超过限制", http.StatusBadRequest)
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "文本长度超过限制"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,24 +280,24 @@ func (h *TTSHandler) HandleTTS(w http.ResponseWriter, r *http.Request) {
|
|||||||
if len(req.Text) > segmentThreshold && len(req.Text) <= h.config.TTS.MaxTextLength {
|
if len(req.Text) > segmentThreshold && len(req.Text) <= h.config.TTS.MaxTextLength {
|
||||||
log.Printf("文本长度 %d 超过阈值 %d,使用分段处理", len(req.Text), segmentThreshold)
|
log.Printf("文本长度 %d 超过阈值 %d,使用分段处理", len(req.Text), segmentThreshold)
|
||||||
// 如果文本长度超过阈值但小于最大限制,使用分段处理
|
// 如果文本长度超过阈值但小于最大限制,使用分段处理
|
||||||
h.handleSegmentedTTS(w, r, req)
|
h.handleSegmentedTTS(c, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
synthStart := time.Now()
|
synthStart := time.Now()
|
||||||
resp, err := h.ttsService.SynthesizeSpeech(r.Context(), req)
|
resp, err := h.ttsService.SynthesizeSpeech(c.Request.Context(), req)
|
||||||
synthTime := time.Since(synthStart)
|
synthTime := time.Since(synthStart)
|
||||||
log.Printf("TTS合成耗时: %v, 文本长度: %d", synthTime, len(req.Text))
|
log.Printf("TTS合成耗时: %v, 文本长度: %d", synthTime, len(req.Text))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "语音合成失败: "+err.Error(), http.StatusInternalServerError)
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "语音合成失败: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置响应
|
// 设置响应
|
||||||
w.Header().Set("Content-Type", "audio/mpeg")
|
c.Header("Content-Type", "audio/mpeg")
|
||||||
writeStart := time.Now()
|
writeStart := time.Now()
|
||||||
w.Write(resp.AudioContent)
|
c.Writer.Write(resp.AudioContent)
|
||||||
writeTime := time.Since(writeStart)
|
writeTime := time.Since(writeStart)
|
||||||
|
|
||||||
// 记录总耗时
|
// 记录总耗时
|
||||||
@@ -242,7 +307,7 @@ func (h *TTSHandler) HandleTTS(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleSegmentedTTS 处理长文本的分段TTS请求
|
// handleSegmentedTTS 处理长文本的分段TTS请求
|
||||||
func (h *TTSHandler) handleSegmentedTTS(w http.ResponseWriter, r *http.Request, req models.TTSRequest) {
|
func (h *TTSHandler) handleSegmentedTTS(c *gin.Context, req models.TTSRequest) {
|
||||||
segmentStart := time.Now() // 分段处理开始时间
|
segmentStart := time.Now() // 分段处理开始时间
|
||||||
text := req.Text
|
text := req.Text
|
||||||
|
|
||||||
@@ -296,7 +361,7 @@ func (h *TTSHandler) handleSegmentedTTS(w http.ResponseWriter, r *http.Request,
|
|||||||
segStart := time.Now()
|
segStart := time.Now()
|
||||||
|
|
||||||
// 合成该段音频
|
// 合成该段音频
|
||||||
resp, err := h.ttsService.SynthesizeSpeech(r.Context(), segReq)
|
resp, err := h.ttsService.SynthesizeSpeech(c.Request.Context(), segReq)
|
||||||
|
|
||||||
// 记录该段合成耗时
|
// 记录该段合成耗时
|
||||||
segTime := time.Since(segStart)
|
segTime := time.Since(segStart)
|
||||||
@@ -331,7 +396,7 @@ func (h *TTSHandler) handleSegmentedTTS(w http.ResponseWriter, r *http.Request,
|
|||||||
|
|
||||||
// 检查是否有错误发生
|
// 检查是否有错误发生
|
||||||
if err := <-errChan; err != nil {
|
if err := <-errChan; err != nil {
|
||||||
http.Error(w, "语音合成失败: "+err.Error(), http.StatusInternalServerError)
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "语音合成失败: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,16 +410,16 @@ func (h *TTSHandler) handleSegmentedTTS(w http.ResponseWriter, r *http.Request,
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("合并音频失败: %v", err)
|
log.Printf("合并音频失败: %v", err)
|
||||||
http.Error(w, "音频合并失败: "+err.Error(), http.StatusInternalServerError)
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "音频合并失败: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置响应内容类型
|
// 设置响应内容类型
|
||||||
w.Header().Set("Content-Type", "audio/mpeg")
|
c.Header("Content-Type", "audio/mpeg")
|
||||||
|
|
||||||
// 写入合并后的音频数据
|
// 写入合并后的音频数据
|
||||||
totalSize := len(audioData)
|
totalSize := len(audioData)
|
||||||
if _, writeErr := w.Write(audioData); writeErr != nil {
|
if _, writeErr := c.Writer.Write(audioData); writeErr != nil {
|
||||||
log.Printf("写入响应失败: %v", writeErr)
|
log.Printf("写入响应失败: %v", writeErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,7 +533,6 @@ func splitTextBySentences(text string) []string {
|
|||||||
if currentMerged.Len() > 0 {
|
if currentMerged.Len() > 0 {
|
||||||
mergedSentence := currentMerged.String()
|
mergedSentence := currentMerged.String()
|
||||||
mergedSentences = append(mergedSentences, mergedSentence)
|
mergedSentences = append(mergedSentences, mergedSentence)
|
||||||
log.Printf("添加最后剩余的合并句子,长度=%d", utf8.RuneCountInString(mergedSentence))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mergedSentences
|
return mergedSentences
|
||||||
@@ -476,77 +540,3 @@ func splitTextBySentences(text string) []string {
|
|||||||
|
|
||||||
return sentences
|
return sentences
|
||||||
}
|
}
|
||||||
|
|
||||||
// truncateForLog 截断文本用于日志显示,同时显示开头和结尾
|
|
||||||
func truncateForLog(text string, maxLength int) string {
|
|
||||||
// 先去除换行符
|
|
||||||
text = strings.ReplaceAll(text, "\n", " ")
|
|
||||||
text = strings.ReplaceAll(text, "\r", " ")
|
|
||||||
|
|
||||||
runes := []rune(text)
|
|
||||||
if len(runes) <= maxLength {
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
// 计算开头和结尾各显示多少字符
|
|
||||||
halfLength := maxLength / 2
|
|
||||||
return string(runes[:halfLength]) + "..." + string(runes[len(runes)-halfLength:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// audioMerge 音频合并
|
|
||||||
func audioMerge(audioSegments [][]byte) ([]byte, error) {
|
|
||||||
if len(audioSegments) == 0 {
|
|
||||||
return nil, fmt.Errorf("没有音频片段可合并")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 ffmpeg 合并音频
|
|
||||||
tempDir, err := os.MkdirTemp("", "audio_merge_")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
listFile := filepath.Join(tempDir, "concat.txt")
|
|
||||||
lf, err := os.Create(listFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, seg := range audioSegments {
|
|
||||||
segFile := filepath.Join(tempDir, fmt.Sprintf("seg_%d.mp3", i))
|
|
||||||
if err := os.WriteFile(segFile, seg, 0644); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, err := lf.WriteString(fmt.Sprintf("file '%s'\n", segFile)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lf.Close()
|
|
||||||
|
|
||||||
outputFile := filepath.Join(tempDir, "output.mp3")
|
|
||||||
|
|
||||||
cmd := exec.Command("ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", listFile, "-c", "copy", outputFile)
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
mergedData, err := os.ReadFile(outputFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Printf("使用ffmpeg合并完成,总大小: %s", formatFileSize(len(mergedData)))
|
|
||||||
return mergedData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatFileSize 格式化文件大小
|
|
||||||
func formatFileSize(size int) string {
|
|
||||||
switch {
|
|
||||||
case size < 1024:
|
|
||||||
return fmt.Sprintf("%d B", size)
|
|
||||||
case size < 1024*1024:
|
|
||||||
return fmt.Sprintf("%.2f KB", float64(size)/1024.0)
|
|
||||||
case size < 1024*1024*1024:
|
|
||||||
return fmt.Sprintf("%.2f MB", float64(size)/(1024.0*1024.0))
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("%.2f GB", float64(size)/(1024.0*1024.0*1024.0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"tts/internal/tts"
|
"tts/internal/tts"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// VoicesHandler 处理语音列表请求
|
// VoicesHandler 处理语音列表请求
|
||||||
@@ -19,23 +20,17 @@ func NewVoicesHandler(service tts.Service) *VoicesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HandleVoices 处理语音列表请求
|
// HandleVoices 处理语音列表请求
|
||||||
func (h *VoicesHandler) HandleVoices(w http.ResponseWriter, r *http.Request) {
|
func (h *VoicesHandler) HandleVoices(c *gin.Context) {
|
||||||
// 从查询参数中获取语言筛选
|
// 从查询参数中获取语言筛选
|
||||||
locale := r.URL.Query().Get("locale")
|
locale := c.Query("locale")
|
||||||
|
|
||||||
// 获取语音列表
|
// 获取语音列表
|
||||||
voices, err := h.ttsService.ListVoices(r.Context(), locale)
|
voices, err := h.ttsService.ListVoices(c.Request.Context(), locale)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "获取语音列表失败: "+err.Error(), http.StatusInternalServerError)
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "获取语音列表失败: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置内容类型
|
// 返回JSON响应
|
||||||
w.Header().Set("Content-Type", "application/json")
|
c.JSON(http.StatusOK, voices)
|
||||||
|
|
||||||
// 编码为JSON并返回
|
|
||||||
if err := json.NewEncoder(w).Encode(voices); err != nil {
|
|
||||||
http.Error(w, "JSON编码失败", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,58 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OpenAIAuth 中间件验证 OpenAI API 请求的令牌
|
// OpenAIAuth 中间件验证 OpenAI API 请求的令牌
|
||||||
func OpenAIAuth(apiToken string, next http.Handler) http.Handler {
|
func OpenAIAuth(apiToken string) gin.HandlerFunc {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return func(c *gin.Context) {
|
||||||
// 如果没有配置令牌,跳过验证
|
// 如果没有配置令牌,跳过验证
|
||||||
if apiToken == "" {
|
if apiToken == "" {
|
||||||
next.ServeHTTP(w, r)
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取请求头中的 Authorization
|
// 获取请求头中的 Authorization
|
||||||
authHeader := r.Header.Get("Authorization")
|
authHeader := c.GetHeader("Authorization")
|
||||||
if authHeader == "" {
|
if authHeader == "" {
|
||||||
http.Error(w, "未提供授权令牌", http.StatusUnauthorized)
|
c.AbortWithStatusJSON(401, gin.H{"error": "未提供授权令牌"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证格式是否为 "Bearer {token}"
|
// 验证格式是否为 "Bearer {token}"
|
||||||
parts := strings.SplitN(authHeader, " ", 2)
|
parts := strings.SplitN(authHeader, " ", 2)
|
||||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
http.Error(w, "授权格式无效", http.StatusUnauthorized)
|
c.AbortWithStatusJSON(401, gin.H{"error": "授权格式无效"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证令牌是否正确
|
// 验证令牌是否正确
|
||||||
if parts[1] != apiToken {
|
if parts[1] != apiToken {
|
||||||
http.Error(w, "令牌无效", http.StatusUnauthorized)
|
c.AbortWithStatusJSON(401, gin.H{"error": "令牌无效"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 令牌验证通过,继续处理请求
|
// 令牌验证通过,继续处理请求
|
||||||
next.ServeHTTP(w, r)
|
c.Next()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TTSAuth 是用于验证 TTS API 接口的中间件
|
// TTSAuth 是用于验证 TTS API 接口的中间件
|
||||||
func TTSAuth(apiKey string, next http.Handler) http.Handler {
|
func TTSAuth(apiKey string) gin.HandlerFunc {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return func(c *gin.Context) {
|
||||||
// 从查询参数中获取 api_key
|
// 从查询参数中获取 api_key
|
||||||
queryKey := r.URL.Query().Get("api_key")
|
queryKey := c.Query("api_key")
|
||||||
|
|
||||||
// 如果 apiKey 配置为空字符串,表示不需要验证
|
// 如果 apiKey 配置为空字符串,表示不需要验证
|
||||||
if apiKey != "" && queryKey != apiKey {
|
if apiKey != "" && queryKey != apiKey {
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
c.AbortWithStatusJSON(401, gin.H{"error": "未授权访问: 无效的 API 密钥"})
|
||||||
w.Write([]byte("未授权访问: 无效的 API 密钥"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证通过,继续处理请求
|
// 验证通过,继续处理请求
|
||||||
next.ServeHTTP(w, r)
|
c.Next()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import "net/http"
|
import "github.com/gin-gonic/gin"
|
||||||
|
|
||||||
// CORS 处理跨域资源共享
|
// CORS 处理跨域资源共享
|
||||||
func CORS(next http.Handler) http.Handler {
|
func CORS() gin.HandlerFunc {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return func(c *gin.Context) {
|
||||||
// 设置CORS响应头
|
// 设置CORS响应头
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
c.Header("Access-Control-Allow-Origin", "*")
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||||
|
|
||||||
// 如果是预检请求,直接返回200
|
// 如果是预检请求,直接返回200
|
||||||
if r.Method == http.MethodOptions {
|
if c.Request.Method == "OPTIONS" {
|
||||||
w.WriteHeader(http.StatusOK)
|
c.AbortWithStatus(200)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 继续下一个处理器
|
// 继续下一个处理器
|
||||||
next.ServeHTTP(w, r)
|
c.Next()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,45 +2,27 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logger 是一个HTTP中间件,记录请求的详细信息
|
// Logger 是一个HTTP中间件,记录请求的详细信息
|
||||||
func Logger(next http.Handler) http.Handler {
|
func Logger() gin.HandlerFunc {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return func(c *gin.Context) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
// 包装ResponseWriter以捕获状态码
|
// 处理请求
|
||||||
wrapper := &responseWriterWrapper{
|
c.Next()
|
||||||
ResponseWriter: w,
|
|
||||||
statusCode: http.StatusOK,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用下一个处理器
|
|
||||||
next.ServeHTTP(wrapper, r)
|
|
||||||
|
|
||||||
// 记录请求信息
|
// 记录请求信息
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
log.Printf(
|
log.Printf("[%s] %s %s %d %s",
|
||||||
"[%s] %s %s %d %s",
|
c.Request.Method,
|
||||||
r.Method,
|
c.Request.URL.Path,
|
||||||
r.RequestURI,
|
c.ClientIP(),
|
||||||
r.RemoteAddr,
|
c.Writer.Status(),
|
||||||
wrapper.statusCode,
|
|
||||||
duration,
|
duration,
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// responseWriterWrapper 包装http.ResponseWriter以捕获状态码
|
|
||||||
type responseWriterWrapper struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
statusCode int
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteHeader 捕获状态码
|
|
||||||
func (w *responseWriterWrapper) WriteHeader(statusCode int) {
|
|
||||||
w.statusCode = statusCode
|
|
||||||
w.ResponseWriter.WriteHeader(statusCode)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"tts/internal/config"
|
"tts/internal/config"
|
||||||
"tts/internal/http/handlers"
|
"tts/internal/http/handlers"
|
||||||
"tts/internal/http/middleware"
|
"tts/internal/http/middleware"
|
||||||
"tts/internal/tts"
|
"tts/internal/tts"
|
||||||
"tts/internal/tts/microsoft"
|
"tts/internal/tts/microsoft"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetupRoutes 配置所有API路由
|
// SetupRoutes 配置所有API路由
|
||||||
func SetupRoutes(cfg *config.Config, ttsService tts.Service) (http.Handler, error) {
|
func SetupRoutes(cfg *config.Config, ttsService tts.Service) (*gin.Engine, error) {
|
||||||
// 创建一个新的路由多路复用器
|
// 创建Gin路由
|
||||||
mux := http.NewServeMux()
|
router := gin.New()
|
||||||
|
|
||||||
// 创建处理器
|
// 创建处理器
|
||||||
ttsHandler := handlers.NewTTSHandler(ttsService, cfg)
|
ttsHandler := handlers.NewTTSHandler(ttsService, cfg)
|
||||||
@@ -24,43 +25,41 @@ func SetupRoutes(cfg *config.Config, ttsService tts.Service) (http.Handler, erro
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置主页路由
|
// 应用中间件
|
||||||
mux.HandleFunc("/", pagesHandler.HandleIndex)
|
router.Use(middleware.Logger()) // 日志中间件
|
||||||
|
router.Use(middleware.CORS()) // CORS中间件
|
||||||
// 设置API文档路由
|
|
||||||
mux.HandleFunc("/api-doc", pagesHandler.HandleAPIDoc)
|
|
||||||
|
|
||||||
// 设置TTS API路由 - 添加认证中间件
|
|
||||||
ttsHandlerFunc := http.HandlerFunc(ttsHandler.HandleTTS)
|
|
||||||
authenticatedTTSHandler := middleware.TTSAuth(cfg.TTS.ApiKey, ttsHandlerFunc)
|
|
||||||
mux.Handle("/tts", authenticatedTTSHandler)
|
|
||||||
|
|
||||||
// 设置语音列表API路由
|
|
||||||
mux.HandleFunc("/voices", voicesHandler.HandleVoices)
|
|
||||||
|
|
||||||
// 创建OpenAI兼容接口的处理器,添加验证中间件
|
|
||||||
openAIHandler := http.HandlerFunc(ttsHandler.HandleOpenAITTS)
|
|
||||||
authenticatedHandler := middleware.OpenAIAuth(cfg.OpenAI.ApiKey, openAIHandler)
|
|
||||||
|
|
||||||
// 应用OpenAI兼容的路由
|
|
||||||
mux.Handle("/v1/audio/speech", authenticatedHandler)
|
|
||||||
mux.Handle("/audio/speech", authenticatedHandler)
|
|
||||||
|
|
||||||
// 设置静态文件服务
|
|
||||||
fs := http.FileServer(http.Dir("./web/static"))
|
|
||||||
mux.Handle("/static/", http.StripPrefix("/static/", fs))
|
|
||||||
|
|
||||||
// 应用基础路径前缀
|
// 应用基础路径前缀
|
||||||
var handler http.Handler = mux
|
var baseRouter gin.IRoutes
|
||||||
if cfg.Server.BasePath != "" {
|
if cfg.Server.BasePath != "" {
|
||||||
handler = http.StripPrefix(cfg.Server.BasePath, mux)
|
baseRouter = router.Group(cfg.Server.BasePath)
|
||||||
|
} else {
|
||||||
|
baseRouter = router
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用中间件
|
// 设置静态文件服务
|
||||||
handler = middleware.Logger(handler) // 日志中间件
|
baseRouter.Static("/static", "./web/static")
|
||||||
handler = middleware.CORS(handler) // CORS中间件
|
|
||||||
|
|
||||||
return handler, nil
|
// 设置主页路由
|
||||||
|
baseRouter.GET("/", pagesHandler.HandleIndex)
|
||||||
|
|
||||||
|
// 设置API文档路由
|
||||||
|
baseRouter.GET("/api-doc", pagesHandler.HandleAPIDoc)
|
||||||
|
|
||||||
|
// 设置TTS API路由 - 添加认证中间件
|
||||||
|
|
||||||
|
baseRouter.POST("/tts", middleware.TTSAuth(cfg.TTS.ApiKey), ttsHandler.HandleTTS)
|
||||||
|
baseRouter.GET("/tts", middleware.TTSAuth(cfg.TTS.ApiKey), ttsHandler.HandleTTS)
|
||||||
|
|
||||||
|
// 设置语音列表API路由
|
||||||
|
baseRouter.GET("/voices", voicesHandler.HandleVoices)
|
||||||
|
|
||||||
|
// 设置OpenAI兼容接口的处理器,添加验证中间件
|
||||||
|
openAIHandler := middleware.OpenAIAuth(cfg.OpenAI.ApiKey)
|
||||||
|
baseRouter.POST("/v1/audio/speech", openAIHandler, ttsHandler.HandleOpenAITTS)
|
||||||
|
baseRouter.POST("/audio/speech", openAIHandler, ttsHandler.HandleOpenAITTS)
|
||||||
|
|
||||||
|
return router, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitializeServices 初始化所有服务
|
// InitializeServices 初始化所有服务
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ func NewApp(configPath string) (*App, error) {
|
|||||||
return nil, fmt.Errorf("初始化服务失败: %w", err)
|
return nil, fmt.Errorf("初始化服务失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置路由
|
// 设置Gin路由
|
||||||
handler, err := routes.SetupRoutes(cfg, ttsService)
|
router, err := routes.SetupRoutes(cfg, ttsService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("设置路由失败: %w", err)
|
return nil, fmt.Errorf("设置路由失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建HTTP服务器
|
// 创建HTTP服务器
|
||||||
server := New(cfg, handler)
|
server := New(cfg, router)
|
||||||
|
|
||||||
return &App{
|
return &App{
|
||||||
server: server,
|
server: server,
|
||||||
|
|||||||
@@ -3,43 +3,36 @@ package server
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"github.com/gin-gonic/gin"
|
||||||
"time"
|
|
||||||
|
|
||||||
"tts/internal/config"
|
"tts/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server 封装HTTP服务器
|
// Server 封装HTTP服务器
|
||||||
type Server struct {
|
type Server struct {
|
||||||
server *http.Server
|
router *gin.Engine
|
||||||
basePath string
|
basePath string
|
||||||
|
port int
|
||||||
}
|
}
|
||||||
|
|
||||||
// New 创建新的HTTP服务器
|
// New 创建新的HTTP服务器
|
||||||
func New(cfg *config.Config, handler http.Handler) *Server {
|
func New(cfg *config.Config, router *gin.Engine) *Server {
|
||||||
// 创建HTTP服务器
|
|
||||||
httpServer := &http.Server{
|
|
||||||
Addr: fmt.Sprintf(":%d", cfg.Server.Port),
|
|
||||||
Handler: handler,
|
|
||||||
ReadTimeout: time.Duration(cfg.Server.ReadTimeout) * time.Second,
|
|
||||||
WriteTimeout: time.Duration(cfg.Server.WriteTimeout) * time.Second,
|
|
||||||
IdleTimeout: 120 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Server{
|
return &Server{
|
||||||
server: httpServer,
|
router: router,
|
||||||
basePath: cfg.Server.BasePath,
|
basePath: cfg.Server.BasePath,
|
||||||
|
port: cfg.Server.Port,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start 启动HTTP服务器
|
// Start 启动HTTP服务器
|
||||||
func (s *Server) Start() error {
|
func (s *Server) Start() error {
|
||||||
fmt.Printf("服务启动在 %s\n", s.server.Addr)
|
addr := fmt.Sprintf(":%d", s.port)
|
||||||
return s.server.ListenAndServe()
|
return s.router.Run(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown 优雅关闭服务器
|
// Shutdown 优雅关闭服务器
|
||||||
func (s *Server) Shutdown(ctx context.Context) error {
|
func (s *Server) Shutdown(ctx context.Context) error {
|
||||||
fmt.Println("正在关闭HTTP服务器...")
|
fmt.Println("正在关闭HTTP服务器...")
|
||||||
return s.server.Shutdown(ctx)
|
// Gin 本身没有提供 Shutdown 方法,需要手动实现
|
||||||
|
// 这里可以添加自定义的关闭逻辑
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,20 +203,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
// 复制HttpTTS链接按钮点击事件
|
// 复制HttpTTS链接按钮点击事件
|
||||||
copyHttpTtsLinkButton.addEventListener('click', function () {
|
copyHttpTtsLinkButton.addEventListener('click', function () {
|
||||||
const text = textInput.value.trim();
|
const text = "{{java.encodeURI(speakText)}}";
|
||||||
if (!text) {
|
|
||||||
showCustomAlert('请输入要转换的文本', 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const voice = voiceSelect.value;
|
const voice = voiceSelect.value;
|
||||||
const style = styleSelect.value;
|
const style = styleSelect.value;
|
||||||
const rate = rateInput.value;
|
const rate = "{{speakSpeed*4}}"
|
||||||
const pitch = pitchInput.value;
|
const pitch = pitchInput.value;
|
||||||
const apiKey = apiKeyInput.value.trim();
|
const apiKey = apiKeyInput.value.trim();
|
||||||
|
|
||||||
// 构建HttpTTS链接
|
// 构建HttpTTS链接
|
||||||
let httpTtsLink = `${window.location.origin}${config.basePath}/tts?t=${encodeURIComponent(text)}&v=${voice}&r=${rate}&p=${pitch}&s=${style}`;
|
let httpTtsLink = `${window.location.origin}${config.basePath}/tts?t=${text}&v=${voice}&r=${rate}&p=${pitch}&s=${style}`;
|
||||||
|
|
||||||
// 添加API Key参数(如果有)
|
// 添加API Key参数(如果有)
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
|
|||||||
@@ -1152,7 +1152,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
|
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
|
||||||
</svg>
|
</svg>
|
||||||
复制HttpTTS链接
|
导入阅读
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user