summaryrefslogtreecommitdiff
path: root/rbutil/rbutilqt/base/tts.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'rbutil/rbutilqt/base/tts.cpp')
-rw-r--r--rbutil/rbutilqt/base/tts.cpp666
1 files changed, 0 insertions, 666 deletions
diff --git a/rbutil/rbutilqt/base/tts.cpp b/rbutil/rbutilqt/base/tts.cpp
deleted file mode 100644
index 852edc33d0..0000000000
--- a/rbutil/rbutilqt/base/tts.cpp
+++ /dev/null
@@ -1,666 +0,0 @@
1/***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
8 *
9 * Copyright (C) 2007 by Dominik Wenger
10 * $Id$
11 *
12 * All files in this archive are subject to the GNU General Public License.
13 * See the file COPYING in the source tree root for full license agreement.
14 *
15 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
16 * KIND, either express or implied.
17 *
18 ****************************************************************************/
19
20#include "tts.h"
21#include "utils.h"
22#include "rbsettings.h"
23/*********************************************************************
24* TTS Base
25**********************************************************************/
26QMap<QString,QString> TTSBase::ttsList;
27
28TTSBase::TTSBase(QObject* parent): EncTtsSettingInterface(parent)
29{
30
31}
32
33// static functions
34void TTSBase::initTTSList()
35{
36 ttsList["espeak"] = "Espeak TTS Engine";
37 ttsList["flite"] = "Flite TTS Engine";
38 ttsList["swift"] = "Swift TTS Engine";
39#if defined(Q_OS_WIN)
40 ttsList["sapi"] = "Sapi TTS Engine";
41#endif
42#if defined(Q_OS_LINUX)
43 ttsList["festival"] = "Festival TTS Engine";
44#endif
45}
46
47// function to get a specific encoder
48TTSBase* TTSBase::getTTS(QObject* parent,QString ttsName)
49{
50
51 TTSBase* tts;
52#if defined(Q_OS_WIN)
53 if(ttsName == "sapi")
54 {
55 tts = new TTSSapi(parent);
56 return tts;
57 }
58 else
59#endif
60#if defined(Q_OS_LINUX)
61 if (ttsName == "festival")
62 {
63 tts = new TTSFestival(parent);
64 return tts;
65 }
66 else
67#endif
68 if (true) // fix for OS other than WIN or LINUX
69 {
70 tts = new TTSExes(ttsName,parent);
71 return tts;
72 }
73}
74
75// get the list of encoders, nice names
76QStringList TTSBase::getTTSList()
77{
78 // init list if its empty
79 if(ttsList.count() == 0)
80 initTTSList();
81
82 return ttsList.keys();
83}
84
85// get nice name of a specific tts
86QString TTSBase::getTTSName(QString tts)
87{
88 if(ttsList.isEmpty())
89 initTTSList();
90 return ttsList.value(tts);
91}
92
93
94/*********************************************************************
95* General TTS Exes
96**********************************************************************/
97TTSExes::TTSExes(QString name,QObject* parent) : TTSBase(parent)
98{
99 m_name = name;
100
101 m_TemplateMap["espeak"] = "\"%exe\" %options -w \"%wavfile\" \"%text\"";
102 m_TemplateMap["flite"] = "\"%exe\" %options -o \"%wavfile\" -t \"%text\"";
103 m_TemplateMap["swift"] = "\"%exe\" %options -o \"%wavfile\" \"%text\"";
104
105}
106
107void TTSExes::generateSettings()
108{
109 QString exepath =RbSettings::subValue(m_name,RbSettings::TtsPath).toString();
110 if(exepath == "") exepath = findExecutable(m_name);
111
112 insertSetting(eEXEPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,
113 tr("Path to TTS engine:"),exepath,EncTtsSetting::eBROWSEBTN));
114 insertSetting(eOPTIONS,new EncTtsSetting(this,EncTtsSetting::eSTRING,
115 tr("TTS engine options:"),RbSettings::subValue(m_name,RbSettings::TtsOptions)));
116}
117
118void TTSExes::saveSettings()
119{
120 RbSettings::setSubValue(m_name,RbSettings::TtsPath,getSetting(eEXEPATH)->current().toString());
121 RbSettings::setSubValue(m_name,RbSettings::TtsOptions,getSetting(eOPTIONS)->current().toString());
122 RbSettings::sync();
123}
124
125bool TTSExes::start(QString *errStr)
126{
127 m_TTSexec = RbSettings::subValue(m_name,RbSettings::TtsPath).toString();
128 m_TTSOpts = RbSettings::subValue(m_name,RbSettings::TtsOptions).toString();
129
130 m_TTSTemplate = m_TemplateMap.value(m_name);
131
132 QFileInfo tts(m_TTSexec);
133 if(tts.exists())
134 {
135 return true;
136 }
137 else
138 {
139 *errStr = tr("TTS executable not found");
140 return false;
141 }
142}
143
144TTSStatus TTSExes::voice(QString text,QString wavfile, QString *errStr)
145{
146 (void) errStr;
147 QString execstring = m_TTSTemplate;
148
149 execstring.replace("%exe",m_TTSexec);
150 execstring.replace("%options",m_TTSOpts);
151 execstring.replace("%wavfile",wavfile);
152 execstring.replace("%text",text);
153 //qDebug() << "voicing" << execstring;
154 QProcess::execute(execstring);
155 return NoError;
156
157}
158
159bool TTSExes::configOk()
160{
161 QString path = RbSettings::subValue(m_name,RbSettings::TtsPath).toString();
162
163 if (QFileInfo(path).exists())
164 return true;
165
166 return false;
167}
168
169/*********************************************************************
170* TTS Sapi
171**********************************************************************/
172TTSSapi::TTSSapi(QObject* parent) : TTSBase(parent)
173{
174 m_TTSTemplate = "cscript //nologo \"%exe\" /language:%lang /voice:\"%voice\" /speed:%speed \"%options\"";
175 defaultLanguage ="english";
176 m_sapi4 =false;
177}
178
179void TTSSapi::generateSettings()
180{
181 // language
182 QStringList languages = RbSettings::languages();
183 languages.sort();
184 EncTtsSetting* setting =new EncTtsSetting(this,EncTtsSetting::eSTRINGLIST,
185 tr("Language:"),RbSettings::subValue("sapi",RbSettings::TtsLanguage),languages);
186 connect(setting,SIGNAL(dataChanged()),this,SLOT(updateVoiceList()));
187 insertSetting(eLANGUAGE,setting);
188 // voice
189 setting = new EncTtsSetting(this,EncTtsSetting::eSTRINGLIST,
190 tr("Voice:"),RbSettings::subValue("sapi",RbSettings::TtsVoice),getVoiceList(RbSettings::subValue("sapi",RbSettings::TtsLanguage).toString()),EncTtsSetting::eREFRESHBTN);
191 connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceList()));
192 insertSetting(eVOICE,setting);
193 //speed
194 insertSetting(eSPEED,new EncTtsSetting(this,EncTtsSetting::eINT,
195 tr("Speed:"),RbSettings::subValue("sapi",RbSettings::TtsSpeed),-10,10));
196 // options
197 insertSetting(eOPTIONS,new EncTtsSetting(this,EncTtsSetting::eSTRING,
198 tr("Options:"),RbSettings::subValue("sapi",RbSettings::TtsOptions)));
199
200}
201
202void TTSSapi::saveSettings()
203{
204 //save settings in user config
205 RbSettings::setSubValue("sapi",RbSettings::TtsLanguage,getSetting(eLANGUAGE)->current().toString());
206 RbSettings::setSubValue("sapi",RbSettings::TtsVoice,getSetting(eVOICE)->current().toString());
207 RbSettings::setSubValue("sapi",RbSettings::TtsSpeed,getSetting(eSPEED)->current().toInt());
208 RbSettings::setSubValue("sapi",RbSettings::TtsOptions,getSetting(eOPTIONS)->current().toString());
209
210 RbSettings::sync();
211}
212
213void TTSSapi::updateVoiceList()
214{
215 qDebug() << "update voiceList";
216 QStringList voiceList = getVoiceList(getSetting(eLANGUAGE)->current().toString());
217 getSetting(eVOICE)->setList(voiceList);
218 if(voiceList.size() > 0) getSetting(eVOICE)->setCurrent(voiceList.at(0));
219 else getSetting(eVOICE)->setCurrent("");
220}
221
222bool TTSSapi::start(QString *errStr)
223{
224
225 m_TTSOpts = RbSettings::subValue("sapi",RbSettings::TtsOptions).toString();
226 m_TTSLanguage =RbSettings::subValue("sapi",RbSettings::TtsLanguage).toString();
227 m_TTSVoice=RbSettings::subValue("sapi",RbSettings::TtsVoice).toString();
228 m_TTSSpeed=RbSettings::subValue("sapi",RbSettings::TtsSpeed).toString();
229 m_sapi4 = RbSettings::subValue("sapi",RbSettings::TtsUseSapi4).toBool();
230
231 QFile::remove(QDir::tempPath() +"/sapi_voice.vbs");
232 QFile::copy(":/builtin/sapi_voice.vbs",QDir::tempPath() + "/sapi_voice.vbs");
233 m_TTSexec = QDir::tempPath() +"/sapi_voice.vbs";
234
235 QFileInfo tts(m_TTSexec);
236 if(!tts.exists())
237 {
238 *errStr = tr("Could not copy the Sapi-script");
239 return false;
240 }
241 // create the voice process
242 QString execstring = m_TTSTemplate;
243 execstring.replace("%exe",m_TTSexec);
244 execstring.replace("%options",m_TTSOpts);
245 execstring.replace("%lang",m_TTSLanguage);
246 execstring.replace("%voice",m_TTSVoice);
247 execstring.replace("%speed",m_TTSSpeed);
248
249 if(m_sapi4)
250 execstring.append(" /sapi4 ");
251
252 qDebug() << "init" << execstring;
253 voicescript = new QProcess(NULL);
254 //connect(voicescript,SIGNAL(readyReadStandardError()),this,SLOT(error()));
255
256 voicescript->start(execstring);
257 if(!voicescript->waitForStarted())
258 {
259 *errStr = tr("Could not start the Sapi-script");
260 return false;
261 }
262
263 if(!voicescript->waitForReadyRead(300))
264 {
265 *errStr = voicescript->readAllStandardError();
266 if(*errStr != "")
267 return false;
268 }
269
270 voicestream = new QTextStream(voicescript);
271 voicestream->setCodec("UTF16-LE");
272
273 return true;
274}
275
276
277QStringList TTSSapi::getVoiceList(QString language)
278{
279 QStringList result;
280
281 QFile::copy(":/builtin/sapi_voice.vbs",QDir::tempPath() + "/sapi_voice.vbs");
282 m_TTSexec = QDir::tempPath() +"/sapi_voice.vbs";
283
284 QFileInfo tts(m_TTSexec);
285 if(!tts.exists())
286 return result;
287
288 // create the voice process
289 QString execstring = "cscript //nologo \"%exe\" /language:%lang /listvoices";
290 execstring.replace("%exe",m_TTSexec);
291 execstring.replace("%lang",language);
292
293 if(RbSettings::value(RbSettings::TtsUseSapi4).toBool())
294 execstring.append(" /sapi4 ");
295
296 qDebug() << "init" << execstring;
297 voicescript = new QProcess(NULL);
298 voicescript->start(execstring);
299 qDebug() << "wait for started";
300 if(!voicescript->waitForStarted())
301 return result;
302 voicescript->closeWriteChannel();
303 voicescript->waitForReadyRead();
304
305 QString dataRaw = voicescript->readAllStandardError().data();
306 result = dataRaw.split(",",QString::SkipEmptyParts);
307 if(result.size() > 0)
308 {
309 result.sort();
310 result.removeFirst();
311 for(int i = 0; i< result.size();i++)
312 {
313 result[i] = result.at(i).simplified();
314 }
315 }
316
317 delete voicescript;
318 QFile::setPermissions(QDir::tempPath() +"/sapi_voice.vbs",QFile::ReadOwner |QFile::WriteOwner|QFile::ExeOwner
319 |QFile::ReadUser| QFile::WriteUser| QFile::ExeUser
320 |QFile::ReadGroup |QFile::WriteGroup |QFile::ExeGroup
321 |QFile::ReadOther |QFile::WriteOther |QFile::ExeOther );
322 QFile::remove(QDir::tempPath() +"/sapi_voice.vbs");
323 return result;
324}
325
326
327
328TTSStatus TTSSapi::voice(QString text,QString wavfile, QString *errStr)
329{
330 (void) errStr;
331 QString query = "SPEAK\t"+wavfile+"\t"+text+"\r\n";
332 qDebug() << "voicing" << query;
333 *voicestream << query;
334 *voicestream << "SYNC\tbla\r\n";
335 voicestream->flush();
336 voicescript->waitForReadyRead();
337 return NoError;
338}
339
340bool TTSSapi::stop()
341{
342
343 *voicestream << "QUIT\r\n";
344 voicestream->flush();
345 voicescript->waitForFinished();
346 delete voicestream;
347 delete voicescript;
348 QFile::setPermissions(QDir::tempPath() +"/sapi_voice.vbs",QFile::ReadOwner |QFile::WriteOwner|QFile::ExeOwner
349 |QFile::ReadUser| QFile::WriteUser| QFile::ExeUser
350 |QFile::ReadGroup |QFile::WriteGroup |QFile::ExeGroup
351 |QFile::ReadOther |QFile::WriteOther |QFile::ExeOther );
352 QFile::remove(QDir::tempPath() +"/sapi_voice.vbs");
353 return true;
354}
355
356bool TTSSapi::configOk()
357{
358 if(RbSettings::subValue("sapi",RbSettings::TtsVoice).toString().isEmpty())
359 return false;
360 return true;
361}
362/**********************************************************************
363 * TSSFestival - client-server wrapper
364 **********************************************************************/
365TTSFestival::~TTSFestival()
366{
367 stop();
368}
369
370void TTSFestival::generateSettings()
371{
372 // server path
373 QString exepath = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString();
374 if(exepath == "" ) exepath = findExecutable("festival");
375 insertSetting(eSERVERPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,"Path to Festival server:",exepath,EncTtsSetting::eBROWSEBTN));
376
377 // client path
378 QString clientpath = RbSettings::subValue("festival-client",RbSettings::TtsPath).toString();
379 if(clientpath == "" ) clientpath = findExecutable("festival_client");
380 insertSetting(eCLIENTPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,
381 tr("Path to Festival client:"),clientpath,EncTtsSetting::eBROWSEBTN));
382
383 // voice
384 EncTtsSetting* setting = new EncTtsSetting(this,EncTtsSetting::eSTRINGLIST,
385 tr("Voice:"),RbSettings::subValue("festival",RbSettings::TtsVoice),getVoiceList(exepath),EncTtsSetting::eREFRESHBTN);
386 connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceList()));
387 connect(setting,SIGNAL(dataChanged()),this,SLOT(clearVoiceDescription()));
388 insertSetting(eVOICE,setting);
389
390 //voice description
391 setting = new EncTtsSetting(this,EncTtsSetting::eREADONLYSTRING,
392 tr("Voice description:"),"",EncTtsSetting::eREFRESHBTN);
393 connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceDescription()));
394 insertSetting(eVOICEDESC,setting);
395}
396
397void TTSFestival::saveSettings()
398{
399 //save settings in user config
400 RbSettings::setSubValue("festival-server",RbSettings::TtsPath,getSetting(eSERVERPATH)->current().toString());
401 RbSettings::setSubValue("festival-client",RbSettings::TtsPath,getSetting(eCLIENTPATH)->current().toString());
402 RbSettings::setSubValue("festival",RbSettings::TtsVoice,getSetting(eVOICE)->current().toString());
403
404 RbSettings::sync();
405}
406
407void TTSFestival::updateVoiceDescription()
408{
409 // get voice Info with current voice and path
410 QString info = getVoiceInfo(getSetting(eVOICE)->current().toString(),getSetting(eSERVERPATH)->current().toString());
411 getSetting(eVOICEDESC)->setCurrent(info);
412}
413
414void TTSFestival::clearVoiceDescription()
415{
416 getSetting(eVOICEDESC)->setCurrent("");
417}
418
419void TTSFestival::updateVoiceList()
420{
421 QStringList voiceList = getVoiceList(getSetting(eSERVERPATH)->current().toString());
422 getSetting(eVOICE)->setList(voiceList);
423 if(voiceList.size() > 0) getSetting(eVOICE)->setCurrent(voiceList.at(0));
424 else getSetting(eVOICE)->setCurrent("");
425}
426
427void TTSFestival::startServer(QString path)
428{
429 if(!configOk())
430 return;
431
432 if(path == "")
433 path = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString();
434
435 serverProcess.start(QString("%1 --server").arg(path));
436 serverProcess.waitForStarted();
437
438 queryServer("(getpid)",300,path);
439 if(serverProcess.state() == QProcess::Running)
440 qDebug() << "Festival is up and running";
441 else
442 qDebug() << "Festival failed to start";
443}
444
445void TTSFestival::ensureServerRunning(QString path)
446{
447 if(serverProcess.state() != QProcess::Running)
448 {
449 startServer(path);
450 }
451}
452
453bool TTSFestival::start(QString* errStr)
454{
455 (void) errStr;
456 ensureServerRunning();
457 if (!RbSettings::subValue("festival",RbSettings::TtsVoice).toString().isEmpty())
458 queryServer(QString("(voice.select '%1)")
459 .arg(RbSettings::subValue("festival", RbSettings::TtsVoice).toString()));
460
461 return true;
462}
463
464bool TTSFestival::stop()
465{
466 serverProcess.terminate();
467 serverProcess.kill();
468
469 return true;
470}
471
472TTSStatus TTSFestival::voice(QString text, QString wavfile, QString* errStr)
473{
474 qDebug() << text << "->" << wavfile;
475
476 QString path = RbSettings::subValue("festival-client",RbSettings::TtsPath).toString();
477 QString cmd = QString("%1 --server localhost --otype riff --ttw --withlisp --output \"%2\" - ").arg(path).arg(wavfile);
478 qDebug() << cmd;
479
480 QProcess clientProcess;
481 clientProcess.start(cmd);
482 clientProcess.write(QString("%1.\n").arg(text).toAscii());
483 clientProcess.waitForBytesWritten();
484 clientProcess.closeWriteChannel();
485 clientProcess.waitForReadyRead();
486 QString response = clientProcess.readAll();
487 response = response.trimmed();
488 if(!response.contains("Utterance"))
489 {
490 qDebug() << "Could not voice string: " << response;
491 *errStr = tr("engine could not voice string");
492 return Warning;
493 /* do not stop the voicing process because of a single string
494 TODO: needs proper settings */
495 }
496 clientProcess.closeReadChannel(QProcess::StandardError);
497 clientProcess.closeReadChannel(QProcess::StandardOutput);
498 clientProcess.terminate();
499 clientProcess.kill();
500
501 return NoError;
502}
503
504bool TTSFestival::configOk()
505{
506 QString serverPath = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString();
507 QString clientPath = RbSettings::subValue("festival-client",RbSettings::TtsPath).toString();
508
509 bool ret = QFileInfo(serverPath).isExecutable() &&
510 QFileInfo(clientPath).isExecutable();
511 if(RbSettings::subValue("festival",RbSettings::TtsVoice).toString().size() > 0 && voices.size() > 0)
512 ret = ret && (voices.indexOf(RbSettings::subValue("festival",RbSettings::TtsVoice).toString()) != -1);
513 return ret;
514}
515
516QStringList TTSFestival::getVoiceList(QString path)
517{
518 if(!configOk())
519 return QStringList();
520
521 if(voices.size() > 0)
522 {
523 qDebug() << "Using voice cache";
524 return voices;
525 }
526
527 QString response = queryServer("(voice.list)",3000,path);
528
529 // get the 2nd line. It should be (<voice_name>, <voice_name>)
530 response = response.mid(response.indexOf('\n') + 1, -1);
531 response = response.left(response.indexOf('\n')).trimmed();
532
533 voices = response.mid(1, response.size()-2).split(' ');
534
535 voices.sort();
536 if (voices.size() == 1 && voices[0].size() == 0)
537 voices.removeAt(0);
538 if (voices.size() > 0)
539 qDebug() << "Voices: " << voices;
540 else
541 qDebug() << "No voices.";
542
543 return voices;
544}
545
546QString TTSFestival::getVoiceInfo(QString voice,QString path)
547{
548 if(!configOk())
549 return "";
550
551 if(!getVoiceList().contains(voice))
552 return "";
553
554 if(voiceDescriptions.contains(voice))
555 return voiceDescriptions[voice];
556
557 QString response = queryServer(QString("(voice.description '%1)").arg(voice), 3000,path);
558
559 if (response == "")
560 {
561 voiceDescriptions[voice]=tr("No description available");
562 }
563 else
564 {
565 response = response.remove(QRegExp("(description \"*\")", Qt::CaseInsensitive, QRegExp::Wildcard));
566 qDebug() << "voiceInfo w/o descr: " << response;
567 response = response.remove(')');
568 QStringList responseLines = response.split('(', QString::SkipEmptyParts);
569 responseLines.removeAt(0); // the voice name itself
570
571 QString description;
572 foreach(QString line, responseLines)
573 {
574 line = line.remove('(');
575 line = line.simplified();
576
577 line[0] = line[0].toUpper(); // capitalize the key
578
579 int firstSpace = line.indexOf(' ');
580 if (firstSpace > 0)
581 {
582 line = line.insert(firstSpace, ':'); // add a colon between the key and the value
583 line[firstSpace+2] = line[firstSpace+2].toUpper(); // capitalize the value
584 }
585
586 description += line + "\n";
587 }
588 voiceDescriptions[voice] = description.trimmed();
589 }
590
591 return voiceDescriptions[voice];
592}
593
594QString TTSFestival::queryServer(QString query, int timeout,QString path)
595{
596 if(!configOk())
597 return "";
598
599 // this operation could take some time
600 emit busy();
601
602 ensureServerRunning(path);
603
604 qDebug() << "queryServer with " << query;
605 QString response;
606
607 QDateTime endTime;
608 if(timeout > 0)
609 endTime = QDateTime::currentDateTime().addMSecs(timeout);
610
611 /* Festival is *extremely* unreliable. Although at this
612 * point we are sure that SIOD is accepting commands,
613 * we might end up with an empty response. Hence, the loop.
614 */
615 while(true)
616 {
617 QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
618 QTcpSocket socket;
619
620 socket.connectToHost("localhost", 1314);
621 socket.waitForConnected();
622
623 if(socket.state() == QAbstractSocket::ConnectedState)
624 {
625 socket.write(QString("%1\n").arg(query).toAscii());
626 socket.waitForBytesWritten();
627 socket.waitForReadyRead();
628
629 response = socket.readAll().trimmed();
630
631 if (response != "LP" && response != "")
632 break;
633 }
634 socket.abort();
635 socket.disconnectFromHost();
636
637 if(timeout > 0 && QDateTime::currentDateTime() >= endTime)
638 {
639 emit busyEnd();
640 return "";
641 }
642 /* make sure we wait a little as we don't want to flood the server with requests */
643 QDateTime tmpEndTime = QDateTime::currentDateTime().addMSecs(500);
644 while(QDateTime::currentDateTime() < tmpEndTime)
645 QCoreApplication::processEvents(QEventLoop::AllEvents);
646 }
647 if(response == "nil")
648 {
649 emit busyEnd();
650 return "";
651 }
652
653 QStringList lines = response.split('\n');
654 if(lines.size() > 2)
655 {
656 lines.removeFirst();
657 lines.removeLast();
658 }
659 else
660 qDebug() << "Response too short: " << response;
661
662 emit busyEnd();
663 return lines.join("\n");
664
665}
666