From ccce42f959c335d4c63b9b822528202aad326f5b Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 1 Dec 2023 15:55:28 +0100 Subject: [PATCH 1/2] Add new TTS API --- core/api/routes.js | 10 +++ core/api/tts/tts.controller.js | 65 ++++++++++++++++ core/index.js | 4 + core/middleware/ttsRateLimit.js | 47 ++++++++++++ test/core/api/tts/tts.controller.test.js | 91 +++++++++++++++++++++++ test/core/api/tts/voice.mp3 | Bin 0 -> 36362 bytes 6 files changed, 217 insertions(+) create mode 100644 core/api/tts/tts.controller.js create mode 100644 core/middleware/ttsRateLimit.js create mode 100644 test/core/api/tts/tts.controller.test.js create mode 100644 test/core/api/tts/voice.mp3 diff --git a/core/api/routes.js b/core/api/routes.js index c3850ae..80c37f3 100644 --- a/core/api/routes.js +++ b/core/api/routes.js @@ -62,6 +62,16 @@ module.exports.load = function Routes(app, io, controllers, middlewares) { asyncMiddleware(controllers.openAIController.ask), ); + // TTS API + app.post( + '/tts/token', + asyncMiddleware(middlewares.accessTokenInstanceAuth), + middlewares.checkUserPlan('plus'), + middlewares.ttsRateLimit, + asyncMiddleware(controllers.ttsController.getTemporaryToken), + ); + app.get('/tts/generate', asyncMiddleware(controllers.ttsController.generate)); + // user app.post('/users/signup', middlewares.rateLimiter, asyncMiddleware(controllers.userController.signup)); app.post('/users/verify', middlewares.rateLimiter, asyncMiddleware(controllers.userController.confirmEmail)); diff --git a/core/api/tts/tts.controller.js b/core/api/tts/tts.controller.js new file mode 100644 index 0000000..4215f3f --- /dev/null +++ b/core/api/tts/tts.controller.js @@ -0,0 +1,65 @@ +const axios = require('axios'); +const uuid = require('uuid'); + +const { UnauthorizedError } = require('../../common/error'); + +const TTS_TOKEN_PREFIX = 'tts-token:'; + +module.exports = function TTSController(redisClient) { + /** + * @api {get} /tts/generate Generate a mp3 file from a text + * @apiName generate + * @apiGroup TTS + * + * + * @apiQuery {String} text The text to generate + * @apiQuery {String} token Temporary token to have access to + * + * @apiSuccessExample {binary} Success-Response: + * HTTP/1.1 200 OK + */ + async function generate(req, res, next) { + const instanceId = await redisClient.get(`${TTS_TOKEN_PREFIX}:${req.query.token}`); + if (!instanceId) { + throw new UnauthorizedError('Invalid TTS token.'); + } + // Streaming response to client + const { data, headers } = await axios({ + url: process.env.TEXT_TO_SPEECH_URL, + method: 'POST', + body: req.body, + headers: { + authorization: `Bearer ${process.env.TEXT_TO_SPEECH_API_KEY}`, + }, + responseType: 'stream', + }); + res.setHeader('content-type', headers['content-type']); + res.setHeader('content-length', headers['content-length']); + data.pipe(res); + } + + /** + * @api {post} /tts/token Get temporary token to access TTS API + * @apiName getToken + * @apiGroup TTS + * + * @apiSuccessExample {binary} Success-Response: + * HTTP/1.1 200 OK + * + * { + * "token": "ac365e90-78f1-482a-8afa-af326d5647a4" + * } + */ + async function getTemporaryToken(req, res, next) { + const token = uuid.v4(); + await redisClient.set(`${TTS_TOKEN_PREFIX}:${token}`, req.instance.id, { + EX: 5 * 60, // 5 minutes in seconds + }); + res.json({ token }); + } + + return { + generate, + getTemporaryToken, + }; +}; diff --git a/core/index.js b/core/index.js index 284156c..f81bb08 100644 --- a/core/index.js +++ b/core/index.js @@ -52,6 +52,7 @@ const AlexaController = require('./api/alexa/alexa.controller'); const EnedisController = require('./api/enedis/enedis.controller'); const EcowattController = require('./api/ecowatt/ecowatt.controller'); const CameraController = require('./api/camera/camera.controller'); +const TTSController = require('./api/tts/tts.controller'); // Middlewares const TwoFactorAuthMiddleware = require('./middleware/twoFactorTokenAuth'); @@ -69,6 +70,7 @@ const AdminApiAuth = require('./middleware/adminApiAuth'); const OpenAIAuthAndRateLimit = require('./middleware/openAIAuthAndRateLimit'); const CameraStreamAccessKeyAuth = require('./middleware/cameraStreamAccessKeyAuth'); const CheckUserPlan = require('./middleware/checkUserPlan'); +const TTSRateLimit = require('./middleware/ttsRateLimit'); // Routes const routes = require('./api/routes'); @@ -220,6 +222,7 @@ module.exports = async (port) => { redisClient, services.telegramService, ), + ttsController: TTSController(redisClient), }; const middlewares = { @@ -238,6 +241,7 @@ module.exports = async (port) => { openAIAuthAndRateLimit: OpenAIAuthAndRateLimit(logger, legacyRedisClient, db), cameraStreamAccessKeyAuth: CameraStreamAccessKeyAuth(redisClient, logger), checkUserPlan: CheckUserPlan(models.userModel, models.instanceModel, logger), + ttsRateLimit: TTSRateLimit(logger, legacyRedisClient, db), }; routes.load(app, io, controllers, middlewares); diff --git a/core/middleware/ttsRateLimit.js b/core/middleware/ttsRateLimit.js new file mode 100644 index 0000000..5fed37e --- /dev/null +++ b/core/middleware/ttsRateLimit.js @@ -0,0 +1,47 @@ +const { RateLimiterRedis } = require('rate-limiter-flexible'); + +const { TooManyRequestsError } = require('../common/error'); +const asyncMiddleware = require('./asyncMiddleware'); + +const MAX_REQUESTS = parseInt(process.env.TTS_MAX_REQUESTS_PER_MONTH_PER_ACCOUNT, 10); + +module.exports = function TTSRateLimit(logger, redisClient, db) { + const limiter = new RateLimiterRedis({ + storeClient: redisClient, + keyPrefix: 'rate_limit:tts_api', + points: MAX_REQUESTS, // max request per month + duration: 30 * 24 * 60 * 60, // 30 days + }); + return asyncMiddleware(async (req, res, next) => { + const instanceWithAccount = await db.t_account + .join({ + t_instance: { + type: 'INNER', + on: { + account_id: 'id', + }, + }, + }) + .findOne({ + 't_instance.id': req.instance.id, + }); + const uniqueIdentifier = instanceWithAccount.id; + // we check if the current account is rate limited + const limiterResult = await limiter.get(uniqueIdentifier); + if (limiterResult && limiterResult.consumedPoints > MAX_REQUESTS) { + logger.warn(`TTS Rate limit: Account ${uniqueIdentifier} has been querying too much this route`); + throw new TooManyRequestsError('Too many requests this month.'); + } + + // We consume one credit + try { + await limiter.consume(uniqueIdentifier); + } catch (e) { + logger.warn(`TTS Rate limit: Account ${uniqueIdentifier} has been querying too much this route`); + logger.warn(e); + throw new TooManyRequestsError('Too many requests this month.'); + } + + next(); + }); +}; diff --git a/test/core/api/tts/tts.controller.test.js b/test/core/api/tts/tts.controller.test.js new file mode 100644 index 0000000..dc7cb98 --- /dev/null +++ b/test/core/api/tts/tts.controller.test.js @@ -0,0 +1,91 @@ +const request = require('supertest'); +const nock = require('nock'); +const fs = require('fs'); +const path = require('path'); +const { expect } = require('chai'); +const { RateLimiterRedis } = require('rate-limiter-flexible'); + +const configTest = require('../../../tasks/config'); + +const voiceFile = fs.readFileSync(path.join(__dirname, './voice.mp3')); + +describe('TTS API', () => { + before(() => { + process.env.TEXT_TO_SPEECH_URL = 'https://test-tts.com'; + process.env.TEXT_TO_SPEECH_API_KEY = 'my-token'; + }); + it('should get token + get mp3', async () => { + nock(process.env.TEXT_TO_SPEECH_URL, { encodedQueryParams: true }) + .post('/', (body) => true) + .reply(200, voiceFile, { + 'content-type': 'audio/mpeg', + 'content-length': 36362, + }); + await TEST_DATABASE_INSTANCE.t_account.update( + { + id: 'b2d23f66-487d-493f-8acb-9c8adb400def', + }, + { + status: 'active', + }, + ); + const response = await request(TEST_BACKEND_APP) + .post('/tts/token') + .set('Accept', 'application/json') + .set('Authorization', configTest.jwtAccessTokenInstance) + .send() + .expect('Content-Type', /json/) + .expect(200); + expect(response.body).to.have.property('token'); + const responseMp3File = await request(TEST_BACKEND_APP) + .get(`/tts/generate?token=${response.body.token}&text=bonjour`) + .set('Accept', 'application/json') + .set('Authorization', configTest.jwtAccessTokenInstance) + .send() + .expect('Content-Type', 'audio/mpeg') + .expect(200); + expect(responseMp3File.text).to.deep.equal(voiceFile.toString()); + }); + it('should return 401', async () => { + const response = await request(TEST_BACKEND_APP) + .get(`/tts/generate?token=toto&text=bonjour`) + .set('Accept', 'application/json') + .set('Authorization', configTest.jwtAccessTokenInstance) + .send() + .expect('Content-Type', /json/) + .expect(401); + expect(response.body).to.deep.equal({ + error_code: 'UNAUTHORIZED', + status: 401, + }); + }); + it('should return 429, too many requests', async () => { + await TEST_DATABASE_INSTANCE.t_account.update( + { + id: 'b2d23f66-487d-493f-8acb-9c8adb400def', + }, + { + status: 'active', + }, + ); + const limiter = new RateLimiterRedis({ + storeClient: TEST_LEGACY_REDIS_CLIENT, + keyPrefix: 'rate_limit:tts_api', + points: 100, // max request per month + duration: 30 * 24 * 60 * 60, // 30 days + }); + await limiter.consume('b2d23f66-487d-493f-8acb-9c8adb400def', 100); + const response = await request(TEST_BACKEND_APP) + .post('/tts/token') + .set('Accept', 'application/json') + .set('Authorization', configTest.jwtAccessTokenInstance) + .send() + .expect('Content-Type', /json/) + .expect(429); + expect(response.body).to.deep.equal({ + status: 429, + error_code: 'TOO_MANY_REQUESTS', + error_message: 'Too many requests this month.', + }); + }); +}); diff --git a/test/core/api/tts/voice.mp3 b/test/core/api/tts/voice.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..5523080854414ec410d7c94ef5af110e82bd5614 GIT binary patch literal 36362 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

)&J%`4*Av`)P6$vpzH;u4ni5vITO z{dC^`6vuWu0nE4w67FpPgaROr_|cY0vm>^Jn1jXlmbnVyKqaWvpTRXnIuT6kBW^fU zt5gjbzX1?NxQP1vDGz!p4>$-9wXft2#v)X|9*cync5mZDM2@KSo=Z3jvrrtj_I<*I z0Edo9?fQMJSvB3YkNWp3%=-0zb~k83ZZAqcR;1~TAQc-VkP14TMtb2b#N!2}ZY_I( zs^zY)%YK>6T0!MOqI=`K9D^9lIcw3xMN3!6JB;t_Pc?Shti12N;Y?-R8CH z{oQ4X0$9ov6t;l)TPWgN29xA8Q_-`HFl4}}r-^c&i%`36)1ZZ^xf!TUNNDUCNpJ`) zsPO=yM%1e+3aO}EKdo9)HvX41BCf0_AZupE$JX3DZW{0lqQ_V)hXOnuP;Ls?2s0g= z@U9ffW!s57H7N)eGj=1DNbw@mmUI*41`a)$b(5qGlxru7ESLx3Vugww8+BeRXI%gA z7p1&X@8)k5RDEy5GPU>IB5jdVGj8}kNf8_o)bGoYu9lK3oYMM|A`F+bpu13Tuk5Ub z^AiShg=sj0afnfSHYmQn#QeIc6k?yxj9cZZayhH0*>K6j4!gv)X8x~TlHvc%tc65_ zkfM8TJtj^G#(Ez`TKFnHtLH`D3;bq}3R~rk3G^oHjhg&N=skc)&)NSJhW!0n0F}Eb zd&5F5VTZaE$uy5@ihsYn@Dt)FvxGFwAqIU2p?Z>^EunARatj!iruMx2{oKmYykD5l z^5J*?ryj9Fg=wQIx|veFe;s;IuS4Xg1O{U@O7yTfMv{@9O*bBkJ1cVCjW+IxRJ+|G zG#X!qjB_lp{TE@U-I!}wk%L|60&jqWe^EcJUMSqxK%)FksMPlwZGw=K1U|S)6#b<1eYOa`Gh+5vla9rjjR=&VTb@G7@IC z!QYSk{&0~$;E0~UBTmv5A3jdMe$hbat|ea*M$Qh0;j;fFA=fBN6{l+_B2a| zutE=`bdO+CAQK<5C<$LG`Ips*(l4VeXaz_{!}W~IS@p|zqede5fLZ-pyuEu{?jYtmuQVR>SJ3zY4N3KOK9wcvJrg() z%Wk!*y=4V+>gtv$IoROMzUj195{LxZfQJ!`DJA=z1{QnAoyM4Vzr{CPp6z_<%XzZ@ z{v~G`ky_c(Y_w&1tYWh+RJ9-uG?P9yP!v4^SaGHprL@A&eC|NH=>TV7veJSvp6h=;bLBhUY@mo9w%UAm`3} zz5+h*MNAa0Vdb%`1K)&2D<_?9L1XfA%{Jc~@`Q-$`JWG-O=2+h=3Z|1fh&68Ks;}0 z4TK&{qQ)>^$3VAK_+Y?%DwXkxVM*;iQYpgjQe5s~)*h=(uZE)@NDvWYqSJ#A z^D>1YN9kouOdw+yK@r$A!!6J$ewd8LPG;lVD{>s%S1adMVt<8q$t%Ya6?eq?C0{E4 zvthkQQ<;V+H&g^~$ibb(1|B}c^W{j=Xq8m)(dF9e4Zab6CM&teVYsNg_SZAkzHe0O zFc3&mND}S7k4;^dZh7x4;M(VswoJnN(*EJ>PB zac*W`aaL!EUH--%UjN#jx3sKikxlFaO_$W(Qk1}aJByY%{&Yo$KvXkbgTrlZjoSL_ z0xLt_JTpr5Qqx#)T;4O;zkhsh&XGlqJ(;R)XYrcVihIgem_8x4g6+L)*Eb36X*snh zH`115`>;b-mRFB%Fu$4jd+q23mGbEhFO`Bs?X`0GCzRqTxY#7gj~Om&0az)z1VlvO z^6dl+4W42fixxR?=n`Ixq5@YL9i9jldI_w;4Y5ZoMX5&4!ur`vMi2ePo!Q+&&QZ34FMA1+!Iq1rCMGvdOticB`KH~wG-jF?PCf^ z{Wr;-qA78oS=JAT+`bp&y~?f(iNcNthV@%VH%E~Jx7l~k1s(*Z$tZmG(&fuIUgj}n zi~Ko=`AEDyD2)CtT8}D^?dk8&SIuq>XM+V-95*1cI=w9s(&7m z`}*jhb8t+1bVH1NHS?TwZRbp~$Og%M&$0`ln}rj#_E=S% zI~Mqf%yI4_u#+2QSM&VO-0Qzxp{&!&bXWV_-%zJfwE~D+!!fG$W zQ}_EpO#M%}w}M%{casB^4a>5nW*Q1E_=ifL$)Xqaq!(-{3EfMJKCnOA=M&%dA={ad zcJjav7K=0?57;01&>P>iVQQA~d-v@g-#(1rV~akc>hnW)ofyz@DY=$fY7N~RCvYnr z7yfHnjl<6^kO7{LWi4`Ld~}bN-v@cFUGKIY478s~ki>Rk@A?lWs%u$p_$WA7n`= zQwp>1qDAzbjYZ6udgiDA+x1xRl6l7KxT0)c%q^yCCM%?)<5SPO-#NaSU!N!6VDJyP zLS8+)yrq@SSt)6HvHdn}T*lzs;#SV}MgHB(uU?*6Zz|loJbVC@BuTRA?!!@V!dO>Z z;?ta*1dfYCXH&(zJg4B^(C9xGKJW6VV=H(uH=m;)=Dj@ovXvMqO`=#bcv3ihj0ch2 zu{52D%lYoq^xL56@$mv9X3)|iE|fk_9_Nfo{BoOA+U;83cGl-XeA>h1FnUr6dUO5%NPV`@^eBJU)8E$e zfAv~FuHuULw*q%^?Z<3SJFgozN=Nce#W&{0EW6Puz66$Tfm}f@2BWLC#L5~$L~LKV z;}%^E9@U2goLg*qoD{y@dit#<`u!yD&yEn2m*m~ZLkjpPAvC+fTk3_ZnVU183g4Lf z&&~rM=^)EzKD{zN9&(;30M-XjR?x+xD2W7G92%ZHuv6Y4D>XeV%a;5m5?daP`#$DE z2`Hxli+mT$9ZJ9O4IlEes#a=-={E33Yq`$LCyfVo#Schjv{nU2DKvhP-o4y~ccT*6 z_Q3W89p7vr&`MPgJ%KY0Ll8LbC+55ggIR(II6JMjyJOYSyE_H^v2-S*y7=&8pPLyf zzt9))!u_$(dkwc;EF6Ea{)1-+*XOjAL#ye+hWNhN%!mwKwyGRiM;Bhn6T7mor~CGCz)c#Ge=5g=bl;7=ER|3@8hC(=r<{ zN>3q7<|c^fvtt4EIrB2OUA%lYN1r|WP4~>ACE(r9S9?@_9SsIDWB>OoS0Lrd``DE| z9t&PAHp|JwF!HLZOhqcGoQeCJB3OmFBvzWOxirBL)K(gDq?{${D=brCC~>qRsOk$X zOR!;KsgkUtjSE5pEQVnCU~C2U@gSjD?h)`%w?0oPZod(zaf8sK9yxX?UJVv-aUO>qldR>E1&0Vo`@ko&SCfE_|KbU-3VXwE`x zNh~@s$rFu$;B!jsSce)w?cnoX+(-Qnn1^HevT(s5W@x*paEp8?uY)wsnALnS?V8z< z*B9}}&>oE$n(2UYdh%O#nl7hAm3)FP@Fd+?>_2EG!mt>ogA^Dt?2w03DDN>%^kVxd zYewYsoAzh4K4SEk{6~9#57%5z*f|FF2?*w?tbQl?{KsLC_N|D!;Vy+nmw2}v`WmV| zlrOXb05lilS(|t{{rF=K675Z_0)fJW2%(~`Y~7N{w1iYsh&?=9U>cgJ0gUggq(GcQ z%m&v>VW)QxyTk%0HI&3oWZ|B*M=y3V!We65^=ugcoLJ=6*8d3oK$AIpTQ>A}_x)-- zcyl(;gG)C4RNH%OGTu?e0*xZYAwEffQ!f)=Bnk%#S!r<8uealZGP*iZx(Tj74~&Hm zO`l%&alGV+;|p*{)M*r~Cv<$QFruDQEg}iu-|+nGe2_L3C1sP#gTZVBz2Dc~Q1Ps# z{SHQq5F_!4xnITJ;u|E40u{Gch#_EjkkS`$*11LU+`>fnJ93O4s(c$Vx=4R>*|hiRWI;u z&sB}k^GW$B%^B|G$QNR9-ZtPKm3mvHJo6gH!$VfD@z5(m)P$B-OUteA?7vo~|7{Ea zCkL_ttYhJV>iRiY9r)3_P#ax5B^Kdu(lBFoBrdmH zIJyjHVaU34n|66r;UA$3?3^8Eah-irf*%Zrv2(KBgdbsew7OqjF9p@hyrH0bjt%NO zgf6U0+Tj!=J9)5(dcI|0o-B}H87TqR9+cwa*x=_h!|^>?#@EqrR$A|7ieoplddF(g z-?WsEKI-@z@FvCxe5{FAgoTQ|zcmyhzlD*o#1_6a{^ z37uN4hc8JbfN;K$UmOY7U(?fIVJ}dsjQ0eAN zVadl-s|nK;2>XyvL0+o(g-+Qpz%R zZ2Vj+MbAnj!>AjZV81=^n!99ekxF%#){}pPKBH;=e20@fRSXk_^Ib*KdA3^F%{Gi@ zm|AI!3w#+jMOPWoH%Hz?>`4ZVIxKuA$Zj!7S>XabU>fVv%@xZ;8v zP{9aZVhYWU8`XWzhQ-;W6lJRM1aImD$H|)cgttA7V@el77j9qD$^6YWi5j0mF{Rq| zija^sC2P6maJUAwL&dV(4GD1!`$pUbYMV}Lybl#iHt)ISeQur|Uf`n-hK^YW3u_~|j2mrNJ0{~tm&v5y;MQ!i9`Yp$|5CBElCDk^!1sT0*Q6hr!soen!hzSCu6P5^sp*eKCoQ0Tysyp5y zYq(f!Wg~s;gMGndwabI9)d||#I@!J_{p5={<*IHlg;2_@W@wpXHoiWLTT@s`}Hh&b5dnqOLH?@W7@OSyL z+FBweaCA)D&hAWL?Az-uW<~=}>|AQjfd_?$<#?$ze`sN(29~6wscl%6;=?jPsoJ1! zHEI<+CuJ3I3b#I}-3{k;6HDlT)bH8-IZ(wtrQ`jC|XRpvxsT+>1{b6DngiG07@P$!z7 zGx&(F!G1`=phkd642i(vRUpF{dzj8JVK6P2=o8GV^HYD;H4DldkB>|`UwcA&O%1ir zzhXjUum23@eE9U&BQfVz=afHyP$1y%ZV2*0euyQiejh|G%c2V?d4+==H$sXRIt$6D zhr$;?nGqu+q-Nc}fUyg-r6h>|2uZ%8$;GTBI-&qDdH7+ML^JlGiM7+zpjH4Z?GRK} z!rMxj&jP9UVshu11B@dwMTFnhRV-VxLbwmCQw%cv(Y-^JG{Fb@6|NRX^V~M%hon)u zF&B0SIBBj^5vLk6H+4U4^L{Z$cRFGPTuhe{lv}%2;wK2{ofk?O@p$OuZQallN9wwZ zsLn-GE35Uaj7xttZ>}eu!3BmhqFd(d^yk%&<8p$dqSF>jB>2fI3 z^a+E}>Vjyw-%JJDVqUm(grYCI#e(URv#SJ3t*>dwg?!-kY z8}#KHXR!{HSQmqNA8_gwIz4M&Lv?@5Hfu6cr%g4kBP+?)bh)>f+~(p9J}jwnFgKdj zXm>EJb#f`3wwViN49M~QPXTWSz)2JdMv=9B45qN#xM3F!UumZK~eM zlu;QxKTzMc42bx$I^$Z*t$wnf!rZqtB2t1Fk%qPko1^I~0qHdA2 z$ko95>L-lulg_rv2gT|Ou`9g24(m7Nk)E605tC&sy={K1k{JffoN ze=1{U8-slw0ov&#$hKS2P+7~zr?NuRoBZ4zRjop3khBxdcYRjtBa)GCI!g1I zDXj>PLLrrL>viT@rvm)iI5nPQ(K!9Fl)BhLDrco}*zG6GcM02R5`1~560A&&FNSG4 zQ5I<;cV2V+8*{A^)B;j0r{q_b_k^-j()og6f0~MSm6a^&U|zN3YeaJ)_we93GcWR3 zG#1MjEC;$0huL4up!DZ^U@{jaN9<(LDQZ#IaH2A>!%8DhAOX4lx6Q{8l}%jwDGtFe zDIj!rKj5Gbu#(KAVY*h=%f{=m+1era0U;3OV z_mz~b>|bFs=UR6n7Cqj6jfc0i))*)3x$V{Db(76;l?#O}e=2K4Gt+b(%V%QzFL%1b zX|$=tYPA)paMg2a!Rc&A-eFEx1^eTZ<~g(GIfCZSOa6F&qlEXx-{=-taVBI)(Ly^b9gK zRZ5lj5g8icr}TCbchxfgB86E1M9y%aij?g zm}$kr7OSEqRv1?d+=J)z1HPNn+R$$Qc0q_8SPHhB4~ghxP_sY_YYx~)$YOVs)K@^j zp_xCZEuvOa^^133;L0mbn%Yv}iA-C~32R)u>+@l7ge!DhaXZ!h})V7)#NZCq%ZW7}vr}z`S!K9u4z;VCYZtU9_WGUqv zMi!LZjq)IU>MA@}{Y+#ABYrF8U%B~zTr2(Oiv_HmH3B=9W7R!jHp8*;qkjn<0q_It zrB8S1>`$x9c*iYZyJ+ui5$c%~kqPt6%$gDA4W(`~@vd0jt8Y`ox%3rbt%{{JiU+fl z(|RoYD5*$4_8w5&ve*wHgjtj?YDfg`euu&4o5a)_S2Jm5fMPl5)H5624YB!2ou?i} z8SLRXF(xnHfaMAZ7HZ0Y2KdgrYdCC`PN(mw(*zV=Y2uvmvQG?m*3lNdu3#zeD?S%+ zP2k)N4|Bv6Ye@jm(nm=!_5A#nlUK^T!^FO26BqlHcsLx9Za*CK z4#)<#=edMlv?*SS~ZmGWl!j?)VQK? zQDHJIh|@uOO4!P%^zdq5ql0Ln%gZ8eAkZ)gxWx5Ik+~}QyMC$CgsKL5MdCrC4I{DC zZl7hU%}i75@=vnB=<4Ha%Y2i|pLnRg+ZYI&R<(p~7_8e{uYZLnDoBs0p~Z1&gjERJ zmX=R!?}b9^`r5cKAoJAkGW^By7aSMgE+-2wDUuU=8JC~7Yt5b5Zst-WtMG6fJazNB zZ{#`XsJ%{P1{7@d^2W1x!C=R>WeU&&)Pn6GS4;_bkDBo#a8}b%ny^Cz0!rq*uG?r! z&}Z25gps_7JSxY|0@Xva9c2bRE6G%igj~+MAo!&M+^STUVK}w$uL1~{*mS3nq$Lxt zbOMY27li(EX#BkjKwy5cX@OTjg2RL(89TlhtHB?m0gOS>gH4%(0Y0esQYfTih=ne= zI1n2I@x;9}%jBki2=>&b2xsLosr>PB4;Pk z02S`Ek4uS=fCL^+3lR_y)l0k&l^tuJj^6?o*iWe1I{YfNO)|VWs~+gX*C;MFbG0?B zd~2ZGH#A|)=}X=vg1y|WN1kdhSK%hFh#8@fVR}MOY-Qlv)NquC&-#^xwPA_ z{;tU?Z-ozuMdPJ3hJYqFm{+D6K!Me#qb)IwB>TzQBbL)iW8~8~CrB2bBS0=5S(H2< z=KT5UJFoP2pFf&xw>W}^E3*z?1j+|T7Ca~Qzrzk|Id>Zx3$sHeHpLjCZI_{CZMY{_ zRCg6{&Kb+zrZ>@5Ev^RRBY^7AuW$hlcEGgwDZyNh z{30yriN30{a;uu&_!~o;01Bj!%r%=a-K*fE&QLOjBl&`Lf&S+&&d;#U%kR~nYU4f~ zhIPQOwh?5&mkh0N4ItbRGX$&HP~3doa{rg!6frAQf3v^C19u8QDh+D$hYF{g=e8Bx zTck&D9u5v4r>`L3`QIy~BL={Qkg}#f1rNsvWT!`l>xarw#~?$QXq3pr!QG_j(L<0P z!WfsSF436A^q)LfYA&;CN>#}O3J&Zt8sV;S$aWCR(2#32O&mHd$Tb=Z#5{?TjW^7C2$TOylNGatk%&c5&z>COW-dj!aSDaG> z@bIXJd<1}fNfBXg9;rR9ZCqEuKokOAGC3y1k5l3+D+sZc3Qct*$Nhjh}Cf_Lc= z%mG0GQ3D~}V+@%L+UVpK)Qp^4LHLMBq+E4s6ygd{7t_d)GK~{TBy6;I26(6-{3&82 z07OkH5RxY+$fV}TxZ%Q-2|wl_MM%7esm0bKrg7@aZ1Ce%umBJjlbCC&=OV8)8Um#U z)PO3Qi_;wwmH!fg`4gNnvhoi4^T}|Te;28P^JNq|H#s?-4c&d8DoM;$R3E~~4THIw zmWLkQN={+gYuj7G&iiOECF8+nv~K##D&rDXr;!$L?CTWrt}KjLS zdnJg**Up6sW!LQo6D0~ZOrgDgnwV0%^HxhdklY|TU1$l(N9rl$3(D}U_c>wvu;=f_`*+#3U)w@k?q}S-JNBJUU*COdyKXvvCVhH;%ldSB zs*)E3Lc_t-&`U^z;}G=n6|X`h%Th~|Um!Gdd1qcuM(aR3g!O5ddB1r#+Js*ss-OCq zq691)MsPim@rh4J+e}HMbDee*_K?MLbWxkBe3Vh7E@PpH6g{-XR&4nNc0c%{fbQ$S zhutbpML(_gwlZMlt}O?Z|DPUv+Wvo+uxNV?!i0iRYJ>g~Wky+7Jk0R2(OMrNdZeVB z)EFUu303>!fsZv`jF;egtJrx%Vp+rn2ynkh!Xb>w00^nb#vlflsDME;=(X_ z@g*<2RfFee3m^MW%a$wSP&j07q(iV=nd{*=XMKLUyS zmG-E(D&ok~#ZWa~7ZUL(pQR3diSBQ!!-PyE@9Ujr(9TZ|~D9zc1W^ul0@D{t{XQki|HQ zsF)_m_pz|^2BUwfX`fk24?`OoYnEf9MWXi3r=Ml;s7J_>F^R_?|FT;TgT2?dIb#mB zidd*6giUhrBPL1w1WJb^S})UU)5&-EnE{^x9&&3I2-Dr9o86Kmx#5*a+bAMYsg|aC zri=a@aM)l@f86eB;6{>-nY&Jp6kHNFikM8I7^Ri~AT!iFCds29P3&hPmkvUW~ z1^Fqf97KmtJV@92PC{FpB0gb=6x<)?7wV%_YuT{dK7oaC?Hy;;WT=yV$$eHmP9xdJ z*xL~y{|$do$IFa6ClS-MoKZm87RQ4n9bo(^dmDexv-^L!3l7+Tk4zJgLBh!0Sv9Rg z3sC~@F-0p_ZNe)Cs`Vp4@`R7!#AbaDUIz{`c=jnV+a>C-E_nf$oqDgYA`_0hpxptV z;QNAb1VZjVhy5jV@2^#Cs#wj*y<%Cne>m2exLAYKpCqK&YW{6UJvoqEZM-4?32Kd} z@;66@J*LvoQ{VxlnBw75c6w}ZO=Zw9lYkz16AMbuJv6;A_s@4&@*QMU z4S>rBBE-@!1cG{00WOZAdkcc%r_4R0fGwrLe2-8FD)&W2v@uHb$eO2RK&@dlXy>`E z-Prps(tV)9K;2!yaHfv`<-fkv3L$TL;B12ee6;!oLgj|X^J!Zf4-VYaqDAgLwy~}e z={<~^?Bdx|A#sjU$O*ot1g4(C-j%OA2Pb-Ofzpw-n2^{>=CKt;5OM}y*}k$=0w7|2 z?aTA%n_c{~S~RGRI=yKzej(hwo!;e-Xo1NYuPCXBFi_xM*RV$#(j8IHaxYpRb!(wBTh`^VskxD78Mg$f0$+4H$tTk^_ ze=nW*l#{AC_AFJAKqZs9P{nb9)F-3&hhYA)!WL6Q7>>dE7s8c@j}LSwv(47loTbFC z-hjdEDU0l%nLz9*H+pF}&APhk0OH|zYx7`QN!yQZvqendXKv&Xg0no9B{hTaKd{e`~wndytkKff$VG zgy>q1Y;kju|6n+V!M2a4sJ{vlr{U<0Sr+~FWJM7Ax~2MxJG*Rl*vF5nm)xg8ziD@r z#n|H)zB~3G7}1|aWp&WtN@Y@t(f8^z*!J(4vkh6x*UOAzO8$`_xLE1*7lWN<{x$1V zQ~<=l0Qb}l2(sx#ksmFGSst}jg%gilI-v>a4iS==Ssqgi0}9}#C|Cna5gzn~Tj&6u z6tHmt;_8cnPWA^3rhRT*kdC7G8*>T+tjEHzCZL=eTaiWI`nnH8v((>Z5 zMei62?8(;`N{XsaYzW(@p1`c&`9+* zW&^#@Ad=yam#JFe>{Q7TqI;_YR?bIk-XT;;bRp;$YzE1bTG)so;r6T0T{kN`f$X2t zq>n;s8gPs#`<(1n<1_ESIqF*mCtg+1fZjfw4S^A`;O3Ap!w{vlWGu+o>+BP#^3mPx_@=z-5{mCF>`+Z#uCTHj3l4xcj4vNjz3T5$e*9f?MYcMmdi;;7cvwtXvMoB_B8f5J77TFP6IF*{p!#b!g~dh$rLCG)q~nGFbiF10Ljysc zBqEvy51~(v!PzzH)`D+T-e}Og`Y9=>*zNNHT*DX?RB9Mk)s^I^H13-~o|$ipltCU(4W`a78AHWw2)Xc zqOhVJk;{5{ninjSse@iq9l9F+BZmBmQVic#af)`dvlWm36lr6sXb6RLJiuM$J$%RY?rYECR{=q>6`5$cV<)hmAxx1FFh&OT{7Xy3Hl z-dtxrQ>H2SZ8x5n)0rKcl1so+a9_R$*D@q0m@JAkafI*|_0C zkqr)UbI=p4X939dKLR$FgR`C4`X+AD+n9QLLO8nmSt*3tBIRhkB2l% zhlLr!dmDt&D&&!N;sG;&0pYP)?6J|bB$Mc65^mgy*Es3(2zm5rIAouE$K-y=nzs{# zr5>*445Fs~_A9tcsj|Fo+nXY(t2^+;lQk1NH9`OyuhS;L6H!p<6`#^py{ zOgY#aRA7NI?gU_V0w!w88+Aw1cHL$x2|%Vj7I`SsQ74cgq!E2mND!SCyfJa3_5H)G z4?no(g9psr-3}%?Fn?wg`)8+l5;66J7BTGbTrr3AjVhHIQ9=wf5jRYwlzLCGs+VMu z*bsA@2%-VRQ9$ zZ@7ug2q(dr19kp|9z-<1SdK?d>X3mm3ql=XWXH*w81qC+4LkNRmtK@wB#Cd_!WBgF z3Nd{RJ=Ln?C|DiWOCS@!MPbKoz#-Y4z5P(_H)jFZCp~FB362yBM!R=d)ss+!~X)xR+5Y+>C9le%>M_ z2%7K4m2ZD#mS+q11*P7iL#4S?+i&+!CtRtpAC#Ob)seyoa?BY=AM{=j@1o;bBH?hg zGBe_mcB!mj&Nd>!!wi;Rt+kP@R9BcW0b9N%Q+XG`fUQkgeBmpnDKxQ|G1cuZxil$N zOfs*sf-MkRpq}w@>Fm3C0~HpsPxf(ZE)6!{WFGWLfplxx9QM#IP9B(J3AC>?rVj&cuUM~oH{O1 z134hAR6WK-I-6qYTulrbne>8wo5PwE(me0kJ>cO#W*CaA#JenHhLNPaE}QA{u-tIe z_7w($F&lLxlFaP})_SoMuM=BmM$o4gUTMpC*>Uok(aFnGbehOAd>8R;;ckb@QK>A2kNhJ23$#` zXn^vFq}RA}KC9L6^;UX_m^SDKj4W?Z(yz7hH=dO73+_=Ni^zSDG8J?&8pTT+#lhcd zN*TEMj|Nhzost^LW088eiyGy=-$vifT~^G0JEEr>7dAfW#FZ`PfxPq`I&%btAqnBS zN~*@q-{AXStFY)9f_s4NJeN_Hcs(MU`{Yl7yne_O--;UkEjP zcd$|_lrLX)!oUqb)vin?NHJKx{+G}(Ku^$CDXbJH^1SLu;9Ar)oKt|e%V7lY87qbQ z$9A)^rB>i<43;|T_f5$nbASB}H=WmH*m&F$qx$8Z+T?B(mZr##fgu_OZChg*$Oe{j z-|>x1qf>6&H80y02swE+Y`(DhW^!9NXE{mYunKutoR!*SvH$r|R(jNphx)WEc~Q%TE4X(tP&2Pf*Ahu{+j&NLh+^{EGr9Gn25+P_T% z(b_}2O1>-I)vmAiL^|a>C-ib0oBHj#9Y|i?cX2(mnG*-Bm!~Z|?n_!_w&jn=HpMsQ zX=E6aV+EnEj+z*=cnoOEGzV4Z5G^{0_V2Qn%|?N@<%qH3iw*=em4}H5Ou;G2G)eE! zHR$~tUXSk56KB^ixaHnqQziE%2w;OxRK$`mb|caooyH7KM;hnaYu!MRsw-jz-JyK* zM?Egt9K{=tuXfuEyyVK{hQ&+($;6SDxCsuCa(2#Ru17mbUe08AobipEouQug zn?xba4Sb#0g@1}>_vzz0mXxu|F#Zy{^4ITllpoB(2_j%;_P#c~jNO-vIqJ}^RiuZa zM}epGs<1qXR1#6=)CT8#6wCmTyM#b43lqLYVl#pQTPDgf5i|ur5}HQ244XlpCdbsh z5mb52RXVMnK2}gFbgo4_1D6^YC^h(s9*Hu6BR&-D;-OAW^81+c8b>0QmW8^ZLbAQkv|o zHl}8OL`v@MkR(%6(Lko9y1a%KSXJIrs0!3N|7DkYtZ)W{_FX^r$ME2Rd;u0Bo<_fX zOD(ot1>L*n*{S{v14Sk}3gLvRdY<9|I5bj3QnPKeGARiO1Up%rE@|Fun>jfIqCiY1 zB_6T5$&)TpWWDAPUHE~%A8stsi1utY1I>(D`Bg^VIvxS7!j;wA#tcl%es;~IzHkc? zt!}t&9*|d)kmy!Kzy=Tn2YdKT`_roc3yp8~Eko(TpUsj!lpO65XHychyvZTL@6#3k zH9j|HeLVa2@HGj?lYOC+Gj7_F)v?-A1dFTeIvGul%%p-JV2MG`7fuS_ztl&OZy#kGXIQd&^KFipiV01a?Pl8T}=6 z>Q6{-t$b3?jX}jx);U)1B9#D-#YH=IxQ!q+UWytlhd3B!lJ{aJu?vR);fFIS2}&>6 z$hZ8B2@151@n9}@q0nB)Je^b4+#6oAryP);=r>)p81qz0kh~b!MLmxEKv7gQv~;9H zA76McAJyhVU>>6Td0{hjBZZ5DG@MpS`eW ze%9q_g>7IGoR96&*9@W9-dK3bIm^L!*pkJDJGuC1?C=<@msjF18@Ce7BU=&%qo^U1 z9eh<6s$9I%Q7u9feKp{@=ce6bU3w)F1zR1t_=+M2W{T_-DHVpebyC*hF}59(rPZ)= zmC&_`HcTEDU{;(tt{LrH&(hnQOsnMK555=)2`?REWzkhz-t6W7zoJiZ!zdC!-gcA% zxu4stcrV)Ks;l}75X9m-JBDM&*@8Ilh>7mlJ_nb%MRK5o&;0efEXSqIEGVo<%@v!p zI@LT%GU4|p<+9!Gu}bXg@&wk@N?~>5B)qM$iZJpk*!Qf#f4KTw1Au})uU;HUx`)}= zT!T~jSG+H??Dmj#s7MxB=ry?nS7q!;6I$5UeC-LW@2Pb?9HR$rfzFLggvvHVW~s&! zJTtYR)$0io?LYIPpVncI&M&%_JBD6R=tRjr-R^BOm)u!<)j^RzI-sBkvV)zpkr8NeuwHhVBQ&<-(y$& zbrZ6fUqop6yH_cQ%nZ3arP`!Kj=EfnOSmu!142ft`Do=J_EFAY1q0Crv#bwT$BeIZ zq=jEz{9Eu3{A=!?v6Sr#Uv`~C?+2_cT+GajL^Bv{@R8a;%dC+{%vY^K3~?Nb@6KzV zvp}7n?C%b)iUu`6+V)LKle6=pnj;41-e(K{HHQ8zrj`t}f_Ks^u^=s2?>(iU^~$jw zx&^#>fG0BKGWg=0p}MJ;tiXpVpOw~YUQLwGu!UAA=JaTr%>CoZZ<>)`?grT-U&EC7 z{;>U{!RyXH4eq^Rwdvk9;VZirnS!-AqIl`bpWECK}wJ;xF}O) z40T#i?&H|4F&^&GM}C8(*p%1~4OVoHnkuRU`G?37zLnWAiyp3`{@!yoDFt{gtH~B* z8&;~(rY*baTs-N}TjSacmxtm>@#nXr=djWIIipbYPLzm)6*lw;k`|Ma2IeEwnK-dh zW$zh-;)*E!x0(x6+p0AUSPO5z{QSPXu9X;xo<&8^(uJ=xixoSKA0cm962|#I5rX2c zN+&AXGMV*LOqLF9`YDT>LXnHb5};xY50Gnk`Fd28(#THWdKnM?@UQ>az&rLQJ^ZMt%=w(@#c|w}R(yBj0$`ZA z=3T@}EoI7WOwj84Vwd@9AdQ4h4ZQlEMm1836e}Tvj_unn)ZHn8a|!Pw;&h*)f*gEt zU<#AmD=NG>ew(q>>H~E_t1*2+K{MiNU#rQ5q$sxjK!i86&R2t*&>9fz3KAiyC;i#=kOs@j--N@bXLVwC;2E8!MlI`1SlZ;r`qRt3-w9YTg9ubzOxRL)b~AUkl9izk*)dYcdUeuC0FoDODwbC`i3&qnQnDGy%q{ig>GmRk7#PMZm1s^2-Xad&L^A9k=ZJu%$tdALY@h$S$)_!b%65{P;|FP*hFiY3%k4JQ~8`c;akX;=LMh0Zcui$zPJ~VyKo&2Tx z4WNEb+(c^orZ^1vo-GfPtN06~dheW^$I$ z#1KO&LjS0*^kt?K|L%Q>mPJ=8lw=xgAR!@&%PDLEOe5TjrA zm(Uh~cvGLh#x&yZxg1ZaVJgra!DZ8Aah8MUL;W&HC{DOI`UYd zVkoP_+hyE2r0TBx{sA>iDLnizH!7xLNY03Lfgj^Lr#lh(&XYWbYtT=dz3TU>r#&~} zp7q_R#k?g823oRcTt@*s86cp0Bz-h0hfw2j->q`gIQcSu)L^U~-mzfzoQ-(OnI}CM zGj^17$%-KE#*OL|L$X&CS##9dr?=@uYtfzfg7#e(6Rm6|4VT~kGnTeLCoZ|g9CeF{!lVSU z^I@ne%-k*cY?{Z(^PO?m^i$a(Gjmp4d{-jqk0>{HETbVq?H%>xW?@eC=B%G^1NF z+GFo_mqN&#(L`GVW8!KfiU2bP?yik{UCH_y$F}h zlnJ5tMP_?;ewt9Td~6%HXhK9K7YaZRUf}jWGKE`z!cEt=RhvPNWE|#8XB*y^xlmj? z9`kgF3aLL%Vt@=5fTd9lUnbzEgv?TwPNi+O+tmlrMC_H+skQjNvr9$ZcDdB?#HxK*X{_iC~B*a)rHt zeY$j-HVr`c{AqykEIToi{oeF*XVmXAZL7IV@ni5hwy^`UM#~>cWkI!MCxutXOtMQ* zG(o0=cTtDL;)JO8d={iqdX7DJ@B(?s)3V{|INH9eRK>13QouyJdO|=%kbpUj`{Y){ zV3x?Dl>Y-w+>3V66LkpL;~^_^yh;`U2@0vB+$EgBHm!y|C7VU6MV#Ull(wliSZlh4 z9J!5XA!Z8oqcSNJHiY!zj{pE1_;?&W(Cofa04(6Ki`!+%6|+lCOP>_pz>cp=eEW^2 z?hcDZH_-G36<@9^q&oJ-0pO84+oMKeeKPn(J~$SnVzb~qR>YHl#0g+1N9lj4fqNhL zhKB9qnKz4^P3zIkOt?ZjEiDE85MSE+b=cb97@GGds&;0Z=yaA~V>`M{&h?D^1-~|= zw_dD1GSmOgLy#z9r#m==k+y`~aDvnoxbSLyzNAkz`V5(bW1}p4$09KFmVGh|^9yFZ zo*#>8^ZdeoPg{kjw7D;@l&e7ErbGd)B|LAp(NL`%Clie{%b?!~01+{{2`s1F0Z=JC z!h;VAji4QaK)Qng+GT6V9L5{!$cy2dFiM0BSyYkm@yMW#+Gdok_&f_IXnDn0zGGCx z3ey^?^q5nzaee*-V*}w^Y^`?n`WvGO?{o&px;?$c{NaYTO4v5fk7w)MN9?3X>`+e* zeetVoAO}kZx>AT`?=>&7HMqs}pae$!cxp?b)_O#Sw5Rrhs}7tTQ(Z)_br-;U$o`{$vasO6m~y9B!L{YmFy0X@dbcx#etG}sv1FRmu}r< z$(NO9l37E%Iu<%#wa$dvniOioa6G@-%$Ty=Du?MhSCS{^PW;I79aQR55jC}@wDtHI4$8(u?sIg zcfRlw<>j2Jq$gIan4+YNV_|21ap`&apT=?zDeb4ONJW&((hq3MI|^f+%9Pp z%oc!6m-ibbc`;lT^j#!Kz%%x|R5103S0`mHvw!VuUM){iq^1)NM;sZ6W z$BArOQ+eECX%OMK42d)M*N{-svY-{$!y-j+r<58YuQ-{&OLIN3ql9iK(yUA&Mu{xdKWCbRz_pAt{b6i! zp~^lbVYu)xz8%+WN|#u23WO_O*VTr{^q0`HKf#i#dR6DE>!un@=9A9XPtl2nwJdy0 z>>zy3nkqUhml}Nv7|cF9%!C(`96colfQJGQU3wN}l;6%b{8In*6N)w^TUCc^f$1F{-s?+Bp+#75^$Y0X(Yt1t=saqW zp?tWg$vDtWaoVr98fO%p&gi5m>;}C^QS$X z+N0OE&JE-k)I1?E5Sb6jb?0p^y!1?vSi@ky{)hbfKTkXV^(r8^DC;P~^6O8r*6x1A z(C-=?0)z7}9i4v(;T>9QR&lC6GS+}>&vtC~EYanK-dX^7o2L_|Y#6c%=<2?1?j5wK z5OF8_#`G2U*sdQO5xWlBA}%7MBDZy!BVd}Le1B9#H%f^DsBB0ckN_uQf9GWRMLbHCVO!_8fs+o0X8ll9&uB; z61B!~jItc&6?}ZaZmZhW!I}E!k?qG(U8dp0P-)~+i3PRU&ezCnooSfX3OseJX$%_1 zMuy2dWH|g2nfxl;OR`lx3OvrAU^$AJ5{Z~B?XmN((Oeh4I$BMjRD3-b9!|vVFfOAy z_oJYC$IKH$H;c{WULgv71t{-Fc?-LH5ea>xPL!&oDvM3VZg&1j{3%s^?Nzl2Ducqm zCjEb_^8DZ50s!n{@MxNZT*y4xM|83p4O}gtB!q;`zl5g!v6X3*t0A>q_gn|Jr8S?h z@d*&REg-KD3kiU6REV?Kd1w%11ap83J{i(f9btg~eT#f6HsBD=33sEOtQf%{fZ~8U zm!>eQ1$)4`D^a1PoK@kV+^ec#@b!7e&Ah?#FVzYXwKBwOVy@ksG+mut1i2JX?1b?A z@npI0pJBhxuAYp~-oo{j?Iw&kFu&3Aw4%Sjw2ki(y7=Ix`udx`Z+S^McTxjs=xW&w zHaB`-PWj#Q>9jnDdpnV*ZFHHhzSGfxvgX7F01s3l>})Xeik&}#^sUwQ$4ygw-o2~A z4F>QUntwvEG!qaV&&L$6KBi`lzb;m}pi=b@R>F!KBEldfBe*{(AI=r;5cYh67ce!R z<=vX@0x}N>J9dV_@>V`Q@0S0jdd~bG2qBgH%5uh=j{Y4EPLZlkLM2mgWO(Xvs?jFj zvsA3{i%3b9=0T_^Jftx^aY9X!XE4Tnyp7vaThHiugqVs2z4^oEhI#8!5a>Tk_ob~b z<>fe)g1>}j{K;;UNK{QVhocIQJ_e`MFv4Ijje~ncAnr=53jl!D>jKMu4+u{7EuJ2@ z+KRW#Bn@TWYc}%)hVnvJO;@S&TJoWVW3JS-+=#Pk>`b~4mw7c#H&0K1O$11s9*`1H zC_A0N77x<|T4$~N|UMgO0J#5k(5mwuC~>NEPe$ub4@oDq;8 z;?zr-_(wct)zTy6N}Y3Ej%~ z-tH+a%5LfuBexMRn!1w^q+@IjgCPO@m?2MvRBjg$wtN_q>_WeAg zfgPT@3d9D#*LZ^&!Fj-*JIU~r7E!-y$s`9y)D1X}y+V>29W;$j6laK#U!*H$d^1-G z2E9F@A|YR8Kvai)Ge}9-=fqFAEk+eSBF4og94dc1U-S8Gp3FK7uCZOKx0XdA!fxRU zoQ2EvUqTlEj7u8TYOpGgAou3&>Bge#%CBoCup2k(27ND+CEb#4ub*uaXTY_B)U|Th zHon9c-a!1KBzocfd`_{ki*VfTP#z|UKr=amxQkHinJHBQS)y68l&&Ml*SwH@*OE7` zt$lAqVUyx~=6Hig=x?Q;0`N*m^Ha=Or09W&Kyd320!ttm*&L8bIw0;-D+A5L@IvJx zg-@ytMlpj zdCDKvv}>d4>TAj`BhPPRsYcn_a^F1e@BZ$^ydAqN{h`0?ouminh=H*NVrU7O)w z%3wG!IL!Lvqk(h|&^jPMuvQAQi(Caq3y;nj`NLz>@$=E|dJgGLdsq9wX{Y+q(xyIb>^X%)_I<4+Z1mTxfM!cJd= z_3LeFj&|4hL8eq=*_cfI;qk)4y1I?Rx-F%sW?TH}$Bv6$SzY8NX@TW*({ zsi|n<8Cb&0Ymu4Cx6QTkE75xaB|9pFkGa zVJ3T=7|hFv=AQbIGLEIV!fIOGmNBGN&&LC_pqZ%Tzlm*$iY-G0JR>%k9xcB61a+2s zWK&63zyVT0<2?kFz0w^{W{+(Ikeu$~cHaxT=eZJG$0R>W5>3Rn zerIjgLcAAzrF(|eKMYaMnpd@)Yx^!4ACDJj!FFUA=$1yegpF@cLQwiORHfrbCgF^< zWxVvztkB#vL$)!k0)hpymVEBRaEMkUkvx&!l7(HKtj&@SYjhrMOUtf5?&|g9@e-ffBf?A00Xhb8>@<$0L z2QwSKm_Ibae*{@%xP~hB!oksTH1UgCG4+pNtOzynh(~&rxT5T~wpG?5s|F?gvTQd7 zC8nBp_IysJEwN`IwwDl@kxssk?J{;2;ci%{H#*ZS1YOI_ZR{&D@^twYIaawcSd@_X z#!##HwepRVLRZE4ZM-z(7VEuI7iR;3X$%yh_&S=yLlTT5o~<-n8J+o}9h;!dZHndr zdZDJrvSPE&2#`ra5mt06SLJ+i3|6_st*t^B6>wgQ?Iu0`+8a#A)S<97W9)vZA*Wbu zXCfSd_lsJ=y}98&8)_^iXUo+m(Et*nh31^3vm}C7v|Bw1(` z4uy+0MQ>PnvbJoo)>ef%fnm@5rOmX>C(Y}(mJH)@;Sp5Z3cuj*W zzSg~^w;PEQPsJ+H%t#Loc6c!+LP)mZQ40C;a+DW*!nCB#4v>^yQ`RJwlNusc<=0{l zQtQ$D-5y(>l6aPy8wy^oTUbsmi9{*C@MTC(T9mkp{Y&T+K&YOCR}G##CgP#JZ7F`V z+9IC*LeSVYml~w1FJ;%=CPUzuIxU@O8O@ftk1IBRJO^6jy4@%^(6`Ykj4y}Cv4;gF za4?)MNM%6;aRQ9P*L1EvA(^MB(De%h1e-;)?Uap@`oJRvY z&&#VM)U4N){05ez8m~5>Yi7<8ehJuxYT~ClGRN9yDv=F(r<+V>hJnp`;+%QL}q1 z38$N~v0f{ZP%9?A$!5bU(H5SL8=oVDyqSq%4s3}wE;m!~fn{t+u|i6$t!IoK-W4-A z7!*26>EIz6h?Jej6rLQ1-k@4p<+xoweCcSWeTnbdS08n0=2Me8@GAGi*dfrObIjgM zJi*4pzd*xDu{oh+ZfsUv<{hHm49j)KLdhTgW~b$$#?P(gHQ#2WSUG})nFYG9b+mW{ zV)Qd28m%LSC=2EDi5U~%a0><-Ni?-C!pE815=U1dC2~^(_k6~Y-pMLPY^7HsF=xnY zd^%dqr$#*APuXlhc_9Qd-a`1}A-DyMW=qe>9-IhC`_=g2EiEEce3wTSvz(dD1T}vD zT}T2zCGG@Z8UcIVu&U7X)Q`wZgL{*g2FwWOFvXS`s?8Uvq?#P;RB*#&i6f?o2QL$) zxD|@AM|J|?*yR^vs}ak5Q)Ro^>IA4+83TBvP{T1uA2C=Nf>HmmV7&rRC@(%waI%jW zHJb4jWwa_MhOMcF;YUYUftYR5KwDL^tPSwtaoG+M&@Wb2;oF=N-Wk?p?}G*sUk;Vv z!YZU>E_m-G)hUpww}>qkU4eOoCNCclKBSfDn09hPmuH@M66{na6h!{95ugF}IDaYa*vene{7%Y~qAzY<0R3-iu9=n9hN^EaB2=Z{cF26aUN1^M6>cq6w@xwvYkg@tfYp7Dg1h zUJ39h?c#IIC}Y+X?6Wq-{EeY&07b~j6MH@Th)JEfM_*i)t5G*|VlnDZ0Y=6;Hg*!k zBFGdjE*-T7$7>;F#=-vfs~Tlbwoh^nD=x>LZG#E|z}P5Pk3Y>x-Xy#W4cWFo=4&g5 zDTJfL;iT>xrj!C2+^NYZUBiWBOA;N32arhQP_(r$dUS-~ho?9EFKmJBi1M*OTMW{K zP`vo$IsBq&MjfmU#3+bOvmrQ9ksO@&)LWog^>M`3=Sbl+D&oH>T?(|sX#2ZS&k!W{UDj1>vLi(`F_&zW#-2-fhhNaJ(&;n}G zu(eGbjkP5D7oq=Jy7~V;xIo(v!C*73Oe}e=)A~ea3}`ny1bYR3+&tBZ;9%tuW&4~# z+?&`Qj)wipB)UY)-;bS9N##}Q)!<;cjogO{!4#J1{7T2_fB0Q2_!HiKCMl|${jJEo z<6%W!s@YMzJi_7-7lU)iI>?lgyG-{5QA1RTy|ftUs3=eSYr`!h(Hw=HL4u!2!Vy!I z0Bv_bAD@DchZRIE@$N!C;85+sRp~Kar;Yb2@7MO=2zsd;*i0^Uxd2Ma87%V}GX2vL z(Gpz0#itneIRUPLjK}>Q$jUs3LTQ~{VvyY`!>0o_)3kUPMX%L%*D%UvMuMl?@K8?q z>l^%WgoEoaWegAwXF)>m%fyh+ohrZ4@=$q>@PoX}3Dsqlw1wp)hl-3Nmn+<@c7a@< zL7bq$AuWDtx7KP)8^3r*b09mSyh4h{(&G&^w2E-;*Ywh9m6b;>?Bowc>U%yOuj-lN zZA>fn6%G~gg+(oeyH3{9*_M*e6y)!Hj;73xP>Tv270spb)&I~_5-0-0Ll~6)xRUS2 z)&`YXS2vk2&j;+J%d-FiTC-955`dyi5wtehOSk^rt-lHCMDnYdA4Zl*koKhCLdaFvnja9=_N#B-d8Q0oiV9RK{K)=2k#r|kd zTsu4#nUincNMl#Y$3tv%vV<+{#$)Mz*Xx!pD>j9+P-y&K?$9+j*IO1b3p;0g-RLg0 ze7fo@K(E1+C$K(e1nX_i_(E|LqpJg|^~kDM_p7xpaK6L^;nFjczxcd=9G%X*6sn($ z8aW%Nk=gU}8{oO&KbL25Qzg%=X8xSfuGrrwm#ZE zj?(22Z)@xDAh&I1LB=ho&*5eBxuW)Uc-!czv^IJ)@oh4LN??j^nOy8(c%&2sO{wQ8 z13h;VT3SHJ|C@&8qf-hE8$u>56gcS2#IKsR&WXuKOe64G)=56jg({B(0|caiH{)y( z6PUSRy^Di}FprOb!lMZav)aE+?ToT_ldpfJXKck>_l1- z$K|>2+Wjw`8iKL+Q&-FC9bW1s@aO62XHyHFau%JQIxFgw_urC-e#@M?R^E*Cf1?OK zhxUJigyVYFHIj|qVK(=_PLqyIux~$cn5|XNsSnr^SmcuVI`CA^Lz%chktu2(_ia}4 ztaH6tDjT`YU#FEnaFObJK)htT%m+%T13gi#3lZ+^IA5q_nT&Z)J71dkYp! z+}AK6kom2_o?y3}-w!zWfvYS%kFg|Y98G!ox+1_qhofnijh(;I$}b!eu?y$kQF1$D zIENuw-n8ap+McUT&1zgi>pC1nzuse6tzMgDDf0 zMD^Gr#kXEuHuurWU8|OSUXiYP+3DQ%!%9tBiXp$mJXw}(@=Wm-n4=+86=|ZvmR;cC zkaA3lh6ATa@yu&>d6K16s)h8?1zEE|dFWn)i0NYCWwvHYOSU9$nXEpoRZ45Ov&Pl? zS_+np%jUaI`gi5&<<^gK?NK!;dSP9x_txg=>5Fw1-+lA+jF9Py&bK9#LMBUWS*mkc zh}p%@GO9c8_0t&D$Xg3{@1C`+>`tl4OJR-ep|TpGD}_&}xivq!rSixy>hklwoJ>BK zG-fko%&AInZRF+*S2{CQH)`U7%3c%EqfUxucltb4e9a!@ykyXpQs|+2(jq_EPk+LgkG=S6i6pnCP@Oy=j=_ z99;WK&FSAxx3yt+7wl?2by`zJZ)$}&@Fs8{Eiy9>Qt*??1jW!<1|i+WvP%rk7lm(l z{N>0@dr}P<2=$CrYt@A`4UTO*VXDevbvP)kaMt9Yhy;apA<3J8QzaAG3Ki$1EmM{mS@_48VNgio+Jm4s)a#_^XaDZZ{r1{*%c5cU>+#d`KCJhV>ECUhZ Jqjus50RUMVoU#A_ literal 0 HcmV?d00001 From b6fceb9a0ea8b6b250aee66489dc69f20c0310ad Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 1 Dec 2023 16:01:57 +0100 Subject: [PATCH 2/2] Add URL in get token response --- core/api/tts/tts.controller.js | 10 ++++++++-- test/core/api/tts/tts.controller.test.js | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/core/api/tts/tts.controller.js b/core/api/tts/tts.controller.js index 4215f3f..a9c5f9e 100644 --- a/core/api/tts/tts.controller.js +++ b/core/api/tts/tts.controller.js @@ -43,11 +43,14 @@ module.exports = function TTSController(redisClient) { * @apiName getToken * @apiGroup TTS * + * @apiBody {String} text The text to generate + * * @apiSuccessExample {binary} Success-Response: * HTTP/1.1 200 OK * * { - * "token": "ac365e90-78f1-482a-8afa-af326d5647a4" + * "token": "ac365e90-78f1-482a-8afa-af326d5647a4", + * "url": "https://url_of_the_file" * } */ async function getTemporaryToken(req, res, next) { @@ -55,7 +58,10 @@ module.exports = function TTSController(redisClient) { await redisClient.set(`${TTS_TOKEN_PREFIX}:${token}`, req.instance.id, { EX: 5 * 60, // 5 minutes in seconds }); - res.json({ token }); + const url = `${process.env.GLADYS_PLUS_BACKEND_URL}/tts/generate?token=${token}&text=${encodeURIComponent( + req.body.text, + )}`; + res.json({ token, url }); } return { diff --git a/test/core/api/tts/tts.controller.test.js b/test/core/api/tts/tts.controller.test.js index dc7cb98..ffddbda 100644 --- a/test/core/api/tts/tts.controller.test.js +++ b/test/core/api/tts/tts.controller.test.js @@ -13,6 +13,7 @@ describe('TTS API', () => { before(() => { process.env.TEXT_TO_SPEECH_URL = 'https://test-tts.com'; process.env.TEXT_TO_SPEECH_API_KEY = 'my-token'; + process.env.GLADYS_PLUS_BACKEND_URL = 'http://test-api.com'; }); it('should get token + get mp3', async () => { nock(process.env.TEXT_TO_SPEECH_URL, { encodedQueryParams: true }) @@ -33,10 +34,14 @@ describe('TTS API', () => { .post('/tts/token') .set('Accept', 'application/json') .set('Authorization', configTest.jwtAccessTokenInstance) - .send() + .send({ text: 'Bonjour, je suis Gladys' }) .expect('Content-Type', /json/) .expect(200); expect(response.body).to.have.property('token'); + expect(response.body).to.have.property( + 'url', + `http://test-api.com/tts/generate?token=${response.body.token}&text=Bonjour%2C%20je%20suis%20Gladys`, + ); const responseMp3File = await request(TEST_BACKEND_APP) .get(`/tts/generate?token=${response.body.token}&text=bonjour`) .set('Accept', 'application/json')