diff --git a/.github/workflows/checkPHP.yml b/.github/workflows/checkPHP.yml index 0d68f3ed..eae9699e 100644 --- a/.github/workflows/checkPHP.yml +++ b/.github/workflows/checkPHP.yml @@ -7,6 +7,7 @@ on: jobs: php-lint: + name: PHP Lint — PHP 8.4 runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/checkPHPCompat.yml b/.github/workflows/checkPHPCompat.yml new file mode 100644 index 00000000..6c9e6183 --- /dev/null +++ b/.github/workflows/checkPHPCompat.yml @@ -0,0 +1,40 @@ +name: PHP Compatibility Check + +on: + pull_request: + branches: + - beta + +jobs: + php-compat: + name: PHPStan — PHP ${{ matrix.php-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-version: ['7.4', '8.2', '8.4'] + + steps: + - uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: composer + coverage: none + + - name: Install PHPStan + run: composer global require phpstan/phpstan --no-interaction --quiet + + - name: Checkout Jeedom Core (sparse) + run: | + git clone --depth 1 --filter=blob:none --sparse \ + https://github.com/jeedom/core.git /tmp/jeedom-core + git -C /tmp/jeedom-core sparse-checkout set core/class core/php core/repo + + - name: PHP ${{ matrix.php-version }} Compatibility Check + run: | + PHPSTAN_VERSION=$(echo "${{ matrix.php-version }}" | awk -F. '{printf "%d%02d00", $1, $2}') + printf 'includes:\n - phpstan.neon.dist\nparameters:\n phpVersion: %s\n scanDirectories:\n - /tmp/jeedom-core/core/class\n - /tmp/jeedom-core/core/php\n - /tmp/jeedom-core/core/repo\n' "$PHPSTAN_VERSION" > phpstan.neon + phpstan analyse --level 0 --no-progress core/ desktop/ plugin_info/ diff --git a/.github/workflows/checkPython.yml b/.github/workflows/checkPython.yml new file mode 100644 index 00000000..918b2553 --- /dev/null +++ b/.github/workflows/checkPython.yml @@ -0,0 +1,25 @@ +name: Python Check + +on: + pull_request: + branches: + - beta + +jobs: + python-check: + name: Ruff Lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install ruff + run: pip install ruff + + - name: Ruff Check + run: ruff check resources/ttscastd/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 7c6ff000..341e50cd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,6 +22,7 @@ "authmode", "bigben", "bubbleupnp", + "castloglevel", "chromecast", "chromecasts", "cmdwaittimeout", @@ -31,16 +32,25 @@ "customradios", "customsounds", "customtext", + "defaultpromptresult", "Deutsch", + "Echec", "ehthumbs", "Eiwc", + "ENXIO", "filecontent", "gcast", "GCAST", "gcloudapikey", "gcloudaudioencoding", "gcloudtts", + "geminitts", + "geminittsdefault", + "geminittsenabled", + "geminittsmodel", + "geminittsstyle", "generatetts", + "getdefaultprompt", "Getteur", "github", "gtranslatetts", @@ -62,6 +72,7 @@ "maxrss", "MDNS", "miniplus", + "mkfifo", "multizone", "MUSICTRACK", "mycast", @@ -73,9 +84,12 @@ "newdevice", "newone", "newttscast", + "NONBLOCK", "OLDDIR", "Ondashboard", "Onmobile", + "phpstan", + "PHPSTAN", "purgettscache", "pycache", "refreshcast", @@ -84,8 +98,10 @@ "rtcast", "scanmode", "setvolume", + "shivammathur", "signum", "ssml", + "streamingdefault", "stype", "subdevices", "ttsaiapikey", @@ -112,6 +128,7 @@ "volumeset", "volumeup", "Wavenet", + "WRONLY", "youtube", "ZCONF" ] diff --git a/core/ajax/ttscast.ajax.php b/core/ajax/ttscast.ajax.php index 7e010631..0c7a1992 100644 --- a/core/ajax/ttscast.ajax.php +++ b/core/ajax/ttscast.ajax.php @@ -109,23 +109,25 @@ } log::add('ttscast', 'debug', "[UPLOAD][CustomSound] filename: {$_FILES['fileCustomSound']['name']}"); $extension = strtolower(strrchr($_FILES['fileCustomSound']['name'], '.')); - if (!in_array($extension, array('.mp3'))) { - throw new Exception('[UPLOAD][CustomSound] Extension de fichier non valide (autorisé .mp3) : ' . $extension); + if (!in_array($extension, array('.mp3', '.wav', '.ogg', '.opus', '.flac'))) { + throw new Exception('[UPLOAD][CustomSound] Extension de fichier non valide (autorisé .mp3, .wav, .ogg, .opus, .flac) : ' . $extension); } + $safeFilename = basename($_FILES['fileCustomSound']['name']); + # TODO limiter taille upload mp3 dans les customSounds ? /* if (filesize($_FILES['fileCustomSound']['tmp_name']) > 10000) { throw new Exception(__('[UPLOAD][CustomSound] Le fichier est trop gros (max. 10Ko)', __FILE__)); } */ - $filepath = __DIR__ . "/../../data/media/custom/{$_FILES['fileCustomSound']['name']}"; + $filepath = __DIR__ . "/../../data/media/custom/{$safeFilename}"; log::add('ttscast', 'debug', "[UPLOAD][CustomSound] filepath: {$filepath}"); file_put_contents($filepath, file_get_contents($_FILES['fileCustomSound']['tmp_name'])); if (!file_exists($filepath)) { throw new Exception(__('[UPLOAD][CustomSound] Impossible de sauvegarder le fichier', __FILE__)); } - log::add('ttscast', 'info', "[UPLOAD][CustomSound] Upload OK :: {$_FILES['fileCustomSound']['name']}"); - ajax::success("{$_FILES['fileCustomSound']['name']}"); + log::add('ttscast', 'info', "[UPLOAD][CustomSound] Upload OK :: {$safeFilename}"); + ajax::success("{$safeFilename}"); } if (init('action') == 'uploadCustomRadios') { diff --git a/core/class/ttscast.class.php b/core/class/ttscast.class.php index f42f34ed..232c2fd4 100644 --- a/core/class/ttscast.class.php +++ b/core/class/ttscast.class.php @@ -71,7 +71,7 @@ public static function tts($_filename, $_text) { return true; } else { // file_put_contents($_filename, ''); - log::add('ttscast', 'error', '[generateTTS] You can\'t use Jeedom TTS as engine (in the plugin) and call it from Jeedom TTS API !!'); + log::add('ttscast', 'error', '[generateTTS] Conflit de configuration : le moteur sélectionné est "Jeedom TTS", mais TTSCast est lui-même appelé par l\'API TTS de Jeedom — cela créerait une boucle infinie. Sélectionnez un autre moteur TTS dans la configuration du plugin.'); return false; } @@ -113,15 +113,15 @@ public static function dependancy_info() { $return['state'] = 'in_progress'; } else { if (exec(system::getCmdSudo() . system::get('cmd_check') . '-Ec "python3\-requests|python3\-setuptools|python3\-dev|python3\-venv"') < 4) { - log::add('ttscast', 'debug', '[DepInfo][ERROR] Missing system dependencies'); + log::add('ttscast', 'warning', '[DepInfo] Missing system dependencies'); $return['state'] = 'nok'; } elseif (!file_exists(self::PYTHON3_PATH)) { $return['state'] = 'nok'; } elseif (exec(system::getCmdSudo() . self::PYTHON3_PATH . ' -m pip freeze | grep -Eiwc "' . config::byKey('pythonDepString', 'ttscast', '', true) . '"') < config::byKey('pythonDepNum', 'ttscast', 0, true)) { - log::add('ttscast', 'debug', '[DepInfo][ERROR] Missing Python dependencies'); + log::add('ttscast', 'warning', '[DepInfo] Missing Python dependencies'); $return['state'] = 'nok'; } else { - log::add('ttscast', 'debug', '[DepInfo][INFO] All dependencies are installed'); + log::add('ttscast', 'info', '[DepInfo] All dependencies are installed'); $return['state'] = 'ok'; } } @@ -162,6 +162,7 @@ public static function deamon_start() { $path = realpath(__DIR__ . '/../../resources/ttscastd'); $cmd = self::PYTHON3_PATH . " {$path}/ttscastd.py"; $cmd .= ' --loglevel ' . log::convertLogLevel(log::getLogLevel(__CLASS__)); + $cmd .= ' --castloglevel ' . config::byKey('castLogLevel', __CLASS__, 'daemon'); $cmd .= ' --pluginversion ' . config::byKey('pluginVersion', __CLASS__, '0.0.0'); $cmd .= ' --socketport ' . config::byKey('socketport', __CLASS__, '55111'); $cmd .= ' --cyclefactor ' . config::byKey('cyclefactor', __CLASS__, '1'); @@ -189,6 +190,11 @@ public static function deamon_start() { $cmd .= ' --aidefaulttone "' . config::byKey('ttsAIDefaultTone', __CLASS__, 'NoDefaultTone') . '"'; $cmd .= ' --aiusecustomsysprompt ' . config::byKey('ttsAIUseCustomSysPrompt', __CLASS__, '0'); $cmd .= ' --aicustomsysprompt "' . config::byKey('ttsAICustomSysPrompt', __CLASS__, 'NoCustomSysPrompt') . '"'; + $cmd .= ' --geminittsenabled ' . config::byKey('geminiTTSEnabled', __CLASS__, '0'); + $cmd .= ' --geminittsmodel ' . config::byKey('geminiTTSModel', __CLASS__, 'noModel'); + $cmd .= ' --geminittsdefault ' . config::byKey('geminiTTSDefault', __CLASS__, '0'); + $cmd .= ' --geminittsstyle "' . config::byKey('geminiTTSStyle', __CLASS__, '') . '"'; + $cmd .= ' --streamingdefault ' . config::byKey('streamingDefault', __CLASS__, '0'); $cmd .= ' --pid ' . jeedom::getTmpFolder(__CLASS__) . '/deamon.pid'; // ne PAS modifier # log::add(__CLASS__, 'debug', 'Lancement du démon :: ' . $cmd); @@ -321,7 +327,11 @@ public static function playTestTTS() { $ttsSpeed = config::byKey('gCloudTTSSpeed', 'ttscast', '1.0'); $ttsSSML = config::byKey('ttsTestSSML', 'ttscast', '0'); $ttsAI = config::byKey('ttsTestAI', 'ttscast', '0'); - $value = array('cmd' => 'action', 'cmd_action' => 'ttstest', 'ttsEngine' => $ttsEngine, 'ttsLang' => $ttsLang, 'ttsSpeed' => $ttsSpeed, 'ttsText' => $ttsText, 'ttsGoogleName' => $ttsGoogleName, 'ttsVoiceName' => $ttsVoiceName, 'ttsRSSVoiceName' => $ttsRSSVoiceName, 'ttsRSSSpeed' => $ttsRSSSpeed, 'ttsSSML' => $ttsSSML, 'ttsAI' => $ttsAI); + $ttsTestGemini = config::byKey('ttsTestGemini', 'ttscast', '0'); + $ttsGeminiVoiceName = config::byKey('geminiTTSVoice', 'ttscast', 'Aoede'); + $ttsGeminiStyle = config::byKey('ttsTestGeminiStyle', 'ttscast', ''); + $ttsTestStreaming = ($ttsTestGemini == '1') ? config::byKey('ttsTestStreaming', 'ttscast', '0') : '0'; + $value = array('cmd' => 'action', 'cmd_action' => 'ttstest', 'ttsEngine' => $ttsEngine, 'ttsLang' => $ttsLang, 'ttsSpeed' => $ttsSpeed, 'ttsText' => $ttsText, 'ttsGoogleName' => $ttsGoogleName, 'ttsVoiceName' => $ttsVoiceName, 'ttsRSSVoiceName' => $ttsRSSVoiceName, 'ttsGeminiVoiceName' => $ttsGeminiVoiceName, 'ttsGeminiStyle' => $ttsGeminiStyle, 'ttsRSSSpeed' => $ttsRSSSpeed, 'ttsSSML' => $ttsSSML, 'ttsAI' => $ttsAI, 'ttsGemini' => $ttsTestGemini, 'ttsStreaming' => $ttsTestStreaming); self::sendToDaemon($value); } @@ -334,11 +344,19 @@ public static function generateTTS($file=null, $message=null, $options=null) { $ttsEngine = config::byKey('ttsEngine', 'ttscast', 'gtranslatetts'); // jeedomtts | gtranslatetts | gcloudtts $ttsLang = config::byKey('ttsLang', 'ttscast', 'fr-FR'); $ttsSpeed = config::byKey('gCloudTTSSpeed', 'ttscast', '1.0'); + $ttsGeminiVoiceName = config::byKey('geminiTTSVoice', 'ttscast', 'Aoede'); $ttsOptions = $options; - log::add('ttscast', 'debug', '[generateTTS] ttsOptions After Array :: ' . $ttsOptions); - - $value = array('cmd' => 'action', 'cmd_action' => 'generatetts', 'ttsLang' => $ttsLang, 'ttsEngine' => $ttsEngine, 'ttsSpeed' => $ttsSpeed, 'ttsOptions' => $ttsOptions, 'ttsText' => $ttsText, 'ttsFile' => $ttsFile, 'ttsVoiceName' => $ttsVoiceName, 'ttsRSSVoiceName' => $ttsRSSVoiceName, 'ttsRSSSpeed' => $ttsRSSSpeed); + $engineOverride = null; + if (!empty($ttsOptions)) { + $parsedOptions = json_decode('{' . $ttsOptions . '}', true); + if (is_array($parsedOptions) && isset($parsedOptions['engine'])) { + $engineOverride = $parsedOptions['engine']; + } + } + $logEngine = $engineOverride !== null ? $ttsEngine . ' → ' . $engineOverride . ' (override options)' : $ttsEngine; + log::add('ttscast', 'info', '[GenerateTTS] Moteur: ' . $logEngine . ' | Fichier: ' . $ttsFile . ' | Texte: ' . mb_strimwidth($ttsText, 0, 50, '...', 'UTF-8')); + $value = array('cmd' => 'action', 'cmd_action' => 'generatetts', 'ttsLang' => $ttsLang, 'ttsEngine' => $ttsEngine, 'ttsSpeed' => $ttsSpeed, 'ttsOptions' => $ttsOptions, 'ttsText' => $ttsText, 'ttsFile' => $ttsFile, 'ttsVoiceName' => $ttsVoiceName, 'ttsRSSVoiceName' => $ttsRSSVoiceName, 'ttsRSSSpeed' => $ttsRSSSpeed, 'ttsGeminiVoiceName' => $ttsGeminiVoiceName); self::sendToDaemon($value); } @@ -351,39 +369,31 @@ public static function playTTS($gHome=null, $message=null, $options=null, $cmdNo $ttsEngine = config::byKey('ttsEngine', 'ttscast', 'gtranslatetts'); // jeedomtts | gtranslatetts | gcloudtts $ttsLang = config::byKey('ttsLang', 'ttscast', 'fr-FR'); $ttsSpeed = config::byKey('gCloudTTSSpeed', 'ttscast', '1.0'); + $ttsGeminiVoiceName = config::byKey('geminiTTSVoice', 'ttscast', 'Aoede'); - /* log::add('ttscast', 'debug', '[PlayTTS] Options Before Array :: ' . $options); - - $_appDisableDing = config::byKey('appDisableDing', 'ttscast', false); - if ($_appDisableDing) { - if ($options == null) { - $_resOptions = array(); - } else { - $_resOptions = json_decode("{" . $options . "}", true); + $ttsOptions = $options; + $engineOverride = null; + if (!empty($ttsOptions)) { + $parsedOptions = json_decode('{' . $ttsOptions . '}', true); + if (is_array($parsedOptions) && isset($parsedOptions['engine'])) { + $engineOverride = $parsedOptions['engine']; } - $_resOptions['ding'] = false; - log::add('ttscast', 'debug', '[PlayTTS] _res Ding :: ' . json_encode($_resOptions['ding'])); - $ttsOptions = substr(json_encode($_resOptions), 1, -1); } - else { - $ttsOptions = $options; - } */ - $ttsOptions = $options; - log::add('ttscast', 'debug', '[PlayTTS] ttsOptions After Array :: ' . $ttsOptions); - - $value = array('cmd' => 'action', 'cmd_action' => 'tts', 'ttsLang' => $ttsLang, 'ttsEngine' => $ttsEngine, 'ttsSpeed' => $ttsSpeed, 'ttsOptions' => $ttsOptions, 'ttsText' => $ttsText, 'ttsGoogleUUID' => $ttsGoogleUUID, 'ttsVoiceName' => $ttsVoiceName, 'ttsRSSVoiceName' => $ttsRSSVoiceName, 'ttsRSSSpeed' => $ttsRSSSpeed, 'cmdNotificationId' => $cmdNotificationId); + $logEngine = $engineOverride !== null ? $ttsEngine . ' → ' . $engineOverride . ' (override options)' : $ttsEngine; + log::add('ttscast', 'info', '[PlayTTS] Moteur: ' . $logEngine . ' | UUID: ' . $ttsGoogleUUID . ' | Texte: ' . mb_strimwidth($ttsText, 0, 50, '...', 'UTF-8')); + $value = array('cmd' => 'action', 'cmd_action' => 'tts', 'ttsLang' => $ttsLang, 'ttsEngine' => $ttsEngine, 'ttsSpeed' => $ttsSpeed, 'ttsOptions' => $ttsOptions, 'ttsText' => $ttsText, 'ttsGoogleUUID' => $ttsGoogleUUID, 'ttsVoiceName' => $ttsVoiceName, 'ttsRSSVoiceName' => $ttsRSSVoiceName, 'ttsRSSSpeed' => $ttsRSSSpeed, 'ttsGeminiVoiceName' => $ttsGeminiVoiceName, 'cmdNotificationId' => $cmdNotificationId); self::sendToDaemon($value); } public static function actionGCast($gHomeUUID=null, $action=null, $message=null) { - log::add('ttscast', 'debug', '[ActionGCast] Infos :: ' . $gHomeUUID . ' / ' . $action . " / " . $message); + log::add('ttscast', 'info', '[ActionGCast] Infos :: ' . $gHomeUUID . ' / ' . $action . " / " . $message); $value = array('cmd' => 'action', 'cmd_action' => $action, 'value' => $message, 'googleUUID' => $gHomeUUID); log::add('ttscast', 'debug', '[ActionGCast] ArrayToSend :: ' . json_encode($value)); self::sendToDaemon($value); } public static function mediaGCast($gHomeUUID=null, $action=null, $message=null, $options=null) { - log::add('ttscast', 'debug', '[MediaGCast] Infos :: ' . $gHomeUUID . ' / ' . $action . " / " . $message . " / " . $options); + log::add('ttscast', 'info', '[MediaGCast] Infos :: ' . $gHomeUUID . ' / ' . $action . " / " . $message . " / " . $options); $value = array('cmd' => 'action', 'cmd_action' => $action, 'value' => $message, 'googleUUID' => $gHomeUUID, 'options' => $options); log::add('ttscast', 'debug', '[MediaGCast] ArrayToSend :: ' . json_encode($value)); self::sendToDaemon($value); @@ -417,7 +427,8 @@ public static function customCmdDecoder($customCmd=null) { # Options $optionKeys = [ 'force', 'reload_seconds', 'quit_app', 'playlist', 'enqueue', 'volume', - 'ding', 'wait', 'type', 'ssml', 'genai', 'before', 'voice', 'aitone', 'aisysprompt', 'aitemp' + 'ding', 'wait', 'type', 'ssml', 'markup', 'style', 'genai', 'before', 'voice', 'aitone', 'aisysprompt', 'aitemp', + 'engine', 'streaming' ]; foreach ($optionKeys as $key) { if (array_key_exists($key, $data)) { @@ -455,7 +466,7 @@ public static function getPluginVersion() { } } catch (\Exception $e) { - log::add('ttscast', 'debug', '[Plugin-Version] Get ERROR :: ' . $e->getMessage()); + log::add('ttscast', 'warning', '[Plugin-Version] Get ERROR :: ' . $e->getMessage()); } log::add('ttscast', 'info', '[Plugin-Version] PluginVersion :: ' . $pluginVersion); return $pluginVersion; @@ -486,7 +497,7 @@ public static function getPythonDepFromRequirements() { $pythonDepNum = count($nonEmptyLines); } catch (\Exception $e) { - log::add('ttscast', 'debug', '[Python-Dep] Get requirements.txt ERROR :: ' . $e->getMessage()); + log::add('ttscast', 'warning', '[Python-Dep] Get requirements.txt ERROR :: ' . $e->getMessage()); } log::add('ttscast', 'debug', '[Python-Dep] PythonDepString / PythonDepNum :: ' . $pythonDepString . " / " . $pythonDepNum); config::save('pythonDepString', $pythonDepString, 'ttscast'); @@ -611,12 +622,12 @@ public static function createAndUpdCastFromScan($_data) public static function scheduleUpdateCast($_data) { if (!isset($_data['uuid'])) { - log::add('ttscast', 'error', '[SCHEDULE][CAST] Information manquante (UUID) pour mettre à jour l\'équipement'); + log::add('ttscast', 'warning', '[SCHEDULE][CAST] Information manquante (UUID) pour mettre à jour l\'équipement'); return false; } $updttscast = ttscast::byLogicalId($_data['uuid'], 'ttscast'); if (!is_object($updttscast)) { - log::add('ttscast', 'error', '[SCHEDULE][CAST] Cast non existant dans Jeedom'); + log::add('ttscast', 'warning', '[SCHEDULE][CAST] Équipement introuvable dans Jeedom (UUID : ' . $_data['uuid'] . ') — il a peut-être été supprimé. Relancez un scan pour mettre à jour la liste des équipements.'); return false; } else { @@ -642,18 +653,18 @@ public static function scheduleUpdateCast($_data) public static function realtimeUpdateCast($_data) { if (!isset($_data['uuid'])) { - log::add('ttscast', 'error', '[REALTIME][CAST] Information manquante (UUID) pour mettre à jour l\'équipement'); + log::add('ttscast', 'warning', '[REALTIME][CAST] Information manquante (UUID) pour mettre à jour l\'équipement'); return false; } if (!isset($_data['status_type'])) { - log::add('ttscast', 'error', '[REALTIME][CAST] Information manquante (Status_Type) pour mettre à jour l\'équipement'); + log::add('ttscast', 'warning', '[REALTIME][CAST] Information manquante (Status_Type) pour mettre à jour l\'équipement'); return false; } else { log::add('ttscast', 'debug', '[REALTIME][CAST] Status Type :: ' . $_data['status_type']); } $rtcast = ttscast::byLogicalId($_data['uuid'], 'ttscast'); if (!is_object($rtcast)) { - log::add('ttscast', 'error', '[REALTIME][CAST] Cast non existant dans Jeedom'); + log::add('ttscast', 'warning', '[REALTIME][CAST] Équipement introuvable dans Jeedom (UUID : ' . $_data['uuid'] . ') — il a peut-être été supprimé. Relancez un scan pour mettre à jour la liste des équipements.'); return false; } else { @@ -804,7 +815,7 @@ public function getSoundList() $filesReturn = ''; try { $filesArray = array(); - foreach (glob(dirname(__FILE__) . '/../../data/media/*.mp3') as $fileName) { + foreach (glob(dirname(__FILE__) . '/../../data/media/*.{mp3,wav,ogg,opus,flac}', GLOB_BRACE) as $fileName) { $filesArray[pathinfo($fileName, PATHINFO_BASENAME)] = ucwords(str_replace(["_", "-"], " ", pathinfo($fileName, PATHINFO_FILENAME))); } natsort($filesArray); @@ -823,7 +834,7 @@ public function getCustomSoundList() $filesReturn = ''; try { $filesArray = array(); - foreach (glob(dirname(__FILE__) . '/../../data/media/custom/*.mp3') as $fileName) { + foreach (glob(dirname(__FILE__) . '/../../data/media/custom/*.{mp3,wav,ogg,opus,flac}', GLOB_BRACE) as $fileName) { $filesArray[pathinfo($fileName, PATHINFO_BASENAME)] = ucwords(str_replace(["_", "-"], " ", pathinfo($fileName, PATHINFO_FILENAME))); } natsort($filesArray); @@ -2362,7 +2373,7 @@ public function execute($_options = array()) { log::add('ttscast', 'debug', '[CMD] ' . $logicalId . ' (Custom Decoded Message) :: ' . json_encode($_options)); } else { - log::add('ttscast', 'debug', '[CMD] Il manque un paramètre pour lancer la commande '. $logicalId); + log::add('ttscast', 'warning', '[CMD] customcmd annulé — paramètre \'message\' manquant (commande personnalisée non renseignée). Options reçues : ' . json_encode($_options)); } } @@ -2383,7 +2394,8 @@ public function execute($_options = array()) { ttscast::playTTS($googleUUID, $_options['message'], isset($_options['title']) ? $_options['title'] : null, $cmdNotificationId); } else { - log::add('ttscast', 'debug', '[CMD] Il manque un paramètre pour diffuser un message TTS'); + $missingParam = (!isset($googleUUID)) ? 'UUID de l\'équipement' : 'texte du message'; + log::add('ttscast', 'warning', '[CMD] TTS annulé — paramètre manquant (' . $missingParam . '). Options reçues : ' . json_encode($_options)); } } elseif ($logicalId == 'ai_reformat' && $eqLogic->getLogicalId() == 'TTSCast_AI') { log::add('ttscast', 'debug', '[CMD] ai_reformat :: ' . json_encode($_options)); @@ -2443,7 +2455,7 @@ public function execute($_options = array()) { log::add('ttscast', 'debug', '[CMD] VolumeSet :: ' . $_options['slider'] . ' / ' . $googleUUID); ttscast::actionGCast($googleUUID, "volumeset", $_options['slider']); } else { - log::add('ttscast', 'debug', '[CMD] VolumeSet :: ERROR = Mauvais paramètre'); + log::add('ttscast', 'warning', '[CMD] VolumeSet :: Mauvais paramètre :: UUID=' . ($googleUUID ?? 'null') . ' / ' . json_encode($_options)); } } elseif (in_array($logicalId, ["volumedown", "volumeup", "media_pause", "media_play", "media_stop", "media_previous", "media_next", "media_quit", "media_rewind", "mute_on", "mute_off"])) { log::add('ttscast', 'debug', '[CMD] ' . $logicalId . ' :: ' . json_encode($_options)); @@ -2459,7 +2471,8 @@ public function execute($_options = array()) { ttscast::mediaGCast($googleUUID, $logicalId, $_options['message'], isset($_options['title']) ? $_options['title'] : null); } else { - log::add('ttscast', 'debug', '[CMD] Il manque un paramètre pour lancer la commande '. $logicalId); + $missingParam = (!isset($googleUUID)) ? 'UUID de l\'équipement' : 'URL / chemin du média'; + log::add('ttscast', 'warning', '[CMD] ' . $logicalId . ' annulé — paramètre manquant (' . $missingParam . '). Options reçues : ' . json_encode($_options)); } } elseif (in_array($logicalId, ["radios", "customradios", "sounds", "customsounds"])) { log::add('ttscast', 'debug', '[CMD] ' . $logicalId . ' :: ' . json_encode($_options)); @@ -2469,7 +2482,8 @@ public function execute($_options = array()) { ttscast::mediaGCast($googleUUID, $logicalId, $_options['select'], isset($_options['title']) ? $_options['title'] : null); } else { - log::add('ttscast', 'debug', '[CMD] Il manque un paramètre pour lancer la commande '. $logicalId); + $missingParam = (!isset($googleUUID)) ? 'UUID de l\'équipement' : 'sélection (radio / son)'; + log::add('ttscast', 'warning', '[CMD] ' . $logicalId . ' annulé — paramètre manquant (' . $missingParam . '). Options reçues : ' . json_encode($_options)); } } elseif (in_array($logicalId, ["refresh", "refreshcast"])) { log::add('ttscast', 'debug', '[CMD] ' . $logicalId . ' :: ' . json_encode($_options)); diff --git a/core/i18n/de_DE.json b/core/i18n/de_DE.json index b4564a1b..e6395954 100644 --- a/core/i18n/de_DE.json +++ b/core/i18n/de_DE.json @@ -178,12 +178,14 @@ "Visible": "Visible" }, "plugins\/ttscast\/plugin_info\/configuration.php": { + "Active le moteur de synthèse vocale Gemini TTS. Utilise la clé API Google Gemini ou le compte OAuth2 configuré ci-dessus.": "Aktiviert die Gemini-TTS-Sprachsynthese-Engine. Verwendet den oben konfigurierten Google Gemini-API-Schlüssel oder das OAuth2-Konto.", + "Activer Gemini TTS": "Gemini TTS aktivieren", "Activer IA Générative": "Generative KI aktivieren", "Affiche le prompt système utilisé par l'IA lorsqu'aucun prompt personnalisé n'est défini. Le démon doit être démarré.": "Zeigt die vom KI verwendete Systemaufforderung an, wenn keine benutzerdefinierte Aufforderung festgelegt ist. Der Daemon muss gestartet werden.", "Afficher": "Display", "Ajouter Clé (JSON)": "Add Key (JSON)", "Ajouter des Custom Radios (.json)": "Add Custom Radios (.json)", - "Ajouter un Custom Sound (.mp3)": "Add Custom Sound (.mp3)", + "Ajouter un Custom Sound": "Benutzerdefinierten Ton hinzufügen", "Ajouter un fichier :: Custom Radios": "Add file :: Custom Radios", "Ajouter un fichier :: Custom Sound": "Add file :: Custom Sound", "Allemand (de-DE)": "Deutsch (de-DE)", @@ -191,7 +193,7 @@ "Authentification IA": "KI-Authentifizierung", "Autoriser l'usage de l'IA pour la reformulation des réponses. Désactivez cette option si vous ne souhaitez pas utiliser l'IA": "Erlauben Sie die Verwendung von KI zur Umformulierung von Antworten. Deaktivieren Sie diese Option, wenn Sie KI nicht verwenden möchten", "Ce paramètre est utilisé uniquement pour le test de synthèse vocale (TTS)": "This setting is used only for speech synthesis (TTS) testing", - "Ce paramètre permet de tester la reformulation des réponses à l'aide de l'IA.": "Mit dieser Einstellung können Sie die Umformulierung von Antworten mithilfe der KI testen.", + "Ce paramètre permet de tester la reformulation du texte à l'aide de l'IA avant la synthèse vocale.": "Mit dieser Einstellung können Sie die Umformulierung des Textes mithilfe der KI vor der Sprachsynthese testen.", "Clé API": "API-Schlüssel", "Clé API (Google AI Studio)": "API-Schlüssel (Google AI Studio)", "Clé API (Voice RSS)": "API Key (Voice RSS)", @@ -199,6 +201,8 @@ "Cocher cette case désactivera les messages de mise à jour du plugin dans le centre de message": "Wenn Sie dieses Kästchen ankreuzen, werden die Update-Meldungen des Plugins im Nachrichtencenter deaktiviert.", "Contournement du bug Google TTS de prononciation des mots contenant une apostrophe suivi de caractères accentués": "Umgehung des Google TTS-Fehlers bei der Aussprache von Wörtern, die ein Apostroph gefolgt von Akzentzeichen enthalten", "Convertir les SingleQuote en DoubleQuote": "SingleQuote in DoubleQuote umwandeln", + "Critical": "Kritisch", + "Debug": "Debug", "Demande de génération du TTS de test envoyée (voir les logs du démon pour le résultat)": "Request to generate test TTS :: Sent (See daemon logs for result)", "Demande de mise à jour des listes :: Custom Sounds :: envoyée (voir les logs ttscast)": "Request to update lists :: Custom Sounds :: Sent (See ttscast logs for result)", "Demande de mise à jour des listes :: CustomRadios :: envoyée (voir les logs ttscast)": "Request to update lists :: CustomRadios :: Sent (See ttscast logs for result)", @@ -215,9 +219,11 @@ "Effacer Clé": "Delete Key", "Entrez votre ID de projet Google pour l'authentification avec le moteur d'IA.": "Geben Sie Ihre Google-Projekt-ID zur Authentifizierung bei der KI-Engine ein.", "Entrez votre clé API pour l'authentification avec le moteur d'IA.": "Geben Sie Ihren API-Schlüssel für die Authentifizierung bei der KI-Engine ein.", + "Error": "Fehler", "Espagnol (es-ES)": "Spanish (es-ES)", "Ex: Ceci est un message de test pour la synthèse vocale à partir de Jeedom.": "Ex: This is a test message for speech synthesis from Jeedom.", "Ex: Nest Salon": "Ex: Nest Living Room", + "Ex: Parle d'une voix chaleureuse et rassurante.": "Beispiel: Sprich mit einer warmen und beruhigenden Stimme.", "Ex: Répond à la question s'il y en a une et reformule la phrase": "Beispiel: Beantwortet die Frage, falls es eine gibt, und formuliert den Satz um", "Ex: enthousiaste et humoristique": "Beispiel: enthusiastisch und humorvoll", "Facteur multiplicateur des cycles du démon (Défaut = x1)": "Daemon Cycle Multiplier Factor (Default = x1)", @@ -229,12 +235,19 @@ "Format d'encodage audio pour le moteur Google Cloud TTS (MP3 ou LINEAR16). LINEAR16 offre une meilleure qualité (WAV) mais les fichiers sont plus volumineux.": "Audio-Kodierungsformat für die Google Cloud TTS-Engine (MP3 oder LINEAR16). LINEAR16 bietet eine bessere Qualität (WAV), aber die Dateien sind größer.", "Français (fr-FR)": "French (fr-FR)", "Fréquence des cycles": "Cycles Frequency", + "Gemini 2.5": "Gemini 2.5", + "Gemini 3.x (Recommandé)": "Gemini 3.x (empfohlen)", + "Gemini TTS - Voix multilingues": "Gemini TTS – Mehrsprachige Stimmen", "Google Cloud Text-To-Speech (Clé & Internet)": "Google Cloud Text-To-Speech (Key & Internet)", "Google Translate API (Internet)": "Google Translate API (Internet)", "Génère les fichiers TTS à chaque demande. Il est vivement recommandé de ne PAS cocher cette case, sauf pour faire des tests": "Generates TTS files on each request. It is strongly recommended that you do NOT check this box, except for testing purposes.", "Générer + Diffuser": "Generate + Broadcast", - "IA - Gemini": "KI – Gemini", + "IA & TTS - Gemini": "KI & TTS – Gemini", "Id Projet Google": "Google-Projekt-ID", + "Identique au démon (défaut)": "Identisch mit dem Daemon (Standard)", + "Info": "Info", + "Instruction de style transmise au modèle Gemini TTS (ex: Parle d'une voix chaleureure et rassurante). Laissez vide pour le style par défaut.": "Anweisung zum Stil, die an das Gemini TTS-Modell übermittelt wird (z. B.: Sprich mit einer warmen und beruhigenden Stimme). Lassen Sie das Feld leer, um den Standardstil beizubehalten.", + "Instruction de style transmise au modèle Gemini TTS pour toutes les notifications (ex: Parle d'une voix chaleureuse et rassurante). Peut être surchargé par l'option style: dans les scénarios. Laissez vide pour le style neutre par défaut.": "Anweisung zum Stil, die dem Gemini TTS-Modell für alle Benachrichtigungen übermittelt wird (z. B.: „Sprich mit einer warmen und beruhigenden Stimme“). Kann durch die Option „style:“ in den Szenarien überschrieben werden. Lassen Sie das Feld leer, um den neutralen Standardstil beizubehalten.", "Italien (it-IT)": "Italian (it-IT)", "Jeedom TTS (Local)": "Jeedom TTS (Local)", "LINEAR16 (WAV - 24kHz)": "LINEAR16 (WAV – 24 kHz)", @@ -276,7 +289,9 @@ "Mise à jour des listes :: Custom Sounds": "List Update :: Custom Sounds", "Mise à jour des listes :: Radios": "List Update :: Radios", "Mise à jour des listes :: Sounds": "List Update :: Sounds", - "Modèle IA": "Modell IA", + "Modèle Gemini TTS": "Modell Gemini TTS", + "Modèle Gemini TTS à utiliser. Flash est plus économique, Pro est plus expressif.": "Zu verwendendes Modell: Gemini TTS. Flash ist kostengünstiger, Pro ist ausdrucksstärker.", + "Modèle IA (Reformulation)": "Modell IA (Neufassung)", "Modèles Stables (Recommandés)": "Stabile Modelle (empfohlen)", "Moteur TTS": "TTS Engine", "Moteur TTS à utiliser pour la synthèse vocale": "TTS engine to use for speech synthesis", @@ -285,6 +300,8 @@ "MàJ Radios": "Update Radios", "MàJ Sounds": "Update Sounds", "Ne PAS utiliser le cache": "Do NOT use cache", + "Niveau de log Chromecast": "Chromecast-Protokollstufe", + "Niveau de log des librairies Chromecast du démon. Par défaut, hérite du niveau de log global.": "Protokollierungsstufe der Chromecast-Bibliotheken des Daemons. Standardmäßig wird die globale Protokollierungsstufe übernommen.", "Nombre de jours": "Number of days", "Normal (0)": "Normal (0)", "Normal (1.0)": "Normal (1.0)", @@ -331,27 +348,37 @@ "SITE": "SITE", "Sauvegardez bien votre configuration AVANT d'utiliser le bouton [Générer + Diffuser]": "Sichern Sie Ihre Konfiguration sorgfältig, BEVOR Sie die Schaltfläche [Generieren + Übertragen] verwenden", "Serbe (sr-RS)": "Serbian (sr-RS)", + "Si activé, la lecture audio démarre dès les premières syllabes générées, sans attendre la fin de la synthèse vocale. Réduit la latence perçue.": "Wenn diese Option aktiviert ist, beginnt die Audiowiedergabe bereits bei den ersten generierten Silben, ohne das Ende der Sprachsynthese abzuwarten. Reduziert die wahrgenommene Latenz.", + "Si activé, toutes les notifications utilisent Gemini TTS par défaut.": "Wenn diese Option aktiviert ist, verwenden alle Benachrichtigungen standardmäßig Gemini TTS.", + "Si coché, le test utilise le mode streaming Gemini TTS (lecture directe sans fichier intermédiaire). Nécessite que Gemini TTS soit activé et que le paramètre 'Tester avec Gemini TTS' soit coché.": "Wenn diese Option aktiviert ist, verwendet der Test den Gemini-TTS-Streaming-Modus (direkte Wiedergabe ohne Zwischendatei). Dazu muss Gemini TTS aktiviert und die Option „Mit Gemini TTS testen“ aktiviert sein.", + "Si coché, le test utilise le moteur Gemini TTS au lieu du moteur par défaut. Nécessite que Gemini TTS soit activé.": "Wenn diese Option aktiviert ist, verwendet der Test die Gemini-TTS-Engine anstelle der Standard-Engine. Dazu muss Gemini TTS aktiviert sein.", + "Streaming Gemini TTS par défaut": "Gemini TTS standardmäßig streamen", + "Style de voix (Gemini TTS)": "Stimmstil (Gemini TTS)", + "Style par défaut (Gemini TTS)": "Standardstil (Gemini TTS)", "Sélectionnez le mode d'authentification à utiliser pour se connecter au moteur d'IA.": "Wählen Sie den Authentifizierungsmodus aus, der für die Verbindung mit der KI-Engine verwendet werden soll.", "Sélectionnez le modèle d'IA à utiliser pour la reformulation des réponses.": "Wählen Sie das KI-Modell aus, das für die Umformulierung der Antworten verwendet werden soll.", "TTS (Text To Speech)": "TTS (Text To Speech)", "Temps (entre 0 et 3600 sec) au bout duquel la commande est dans tous les cas exécutée, même si l'équipement Google est encore en cours de lecture (par défaut : 60sec)": "Zeit (zwischen 0 und 3600 Sek.), nach der der Befehl in jedem Fall ausgeführt wird, auch wenn das Google-Gerät noch abgespielt wird (Standard: 60 Sek.)", - "Tester avec l'IA": "Mit KI testen", + "Tester avec Gemini TTS": "Mit Gemini TTS testen", + "Tester avec la reformulation IA": "Mit der KI-Umformulierung testen", "Tester avec la syntaxe SSML (TTS)": "Test with SSML syntax (TTS)", + "Tester avec le mode streaming": "Im Streaming-Modus testen", "Tester la Synthèse Vocale (TTS)": "Test speech synthesis (TTS)", "Tests": "Tests", "Timeout": "Timeout", "Timeout (entre 5 et 300 sec) d'attente de la génération du fichier TTS lors d'un appel via API (par défaut : 30sec)": "Timeout (zwischen 5 und 300 Sek.), bis die TTS-Datei bei einem Aufruf über die SPS generiert wird (Standard: 30 Sek.)", "Timeout de l'API 'GenerateTTS' (secondes)": "Timeout der API 'GenerateTTS' (Sekunden)", - "Ton \/ Style": "Ton \/ Stil", + "Ton de Reformulation": "Umformulierung", "URL Jeedom Externe": "Jeedom External URL", "Upload Clé API (OK) :: ": "Upload API Key (OK) :: ", "Upload Clé API :: Sauvegarde OK": "Upload API Key :: Save OK", "Upload Custom Radios (OK) :: ": "Upload Custom Radios (OK) :: ", "Upload Custom Sound (OK) :: ": "Upload Custom Sound (OK) :: ", "Upload d'un fichier (.json) pour mettre à jour la liste des Custom Radios": "Upload a file (.json) to update the list of Custom Radios", - "Upload un fichier (.mp3) pour l'ajouter au répertoire des Custom Sounds": "Upload a file (.mp3) to add it to the Custom Sounds directory", + "Upload un fichier (.mp3, .wav, .ogg, .opus, .flac) pour l'ajouter au répertoire des Custom Sounds": "Lade eine Datei (.mp3, .wav, .ogg, .opus, .flac) hoch, um sie dem Verzeichnis „Custom Sounds“ hinzuzufügen", "Uploader votre clé JSON en utilisant le bouton 'Ajouter Clé (JSON)'": "Upload your JSON key using the 'Add Key (JSON)' button", "Utilise l'URL externe de Jeedom pour la lecture des fichiers TTS plutôt que l'URL interne (Recommandé = décoché)": "Use the Jeedom external URL for reading TTS files rather than the internal URL (Recommended = unchecked)", + "Utiliser Gemini TTS par défaut": "Gemini TTS standardmäßig verwenden", "Utiliser la Reformulation IA par défaut": "Standardmäßig KI-Umformulierung verwenden", "Valeur par défaut = Normal (0)": "Default Value = Normal (0)", "Valeur par défaut = Normal (1.0)": "Default Value = Normal (1.0)", @@ -367,6 +394,9 @@ "Vitesse de Dictée (Voice RSS TTS)": "Dictation Speed (Voice RSS TTS)", "Vitesse de Dictée (gCloud TTS)": "Dictation Speed (gCloud TTS)", "Voice RSS API (Clé & Internet)": "Voice RSS API (Key & Internet)", + "Voix Gemini TTS": "Gemini TTS-Stimme", + "Voix utilisée par défaut. Toutes les voix Gemini TTS sont multilingues (détection automatique de la langue).": "Standardstimme. Alle Gemini-TTS-Stimmen sind mehrsprachig (automatische Spracherkennung).", + "Warning": "Warnung", "[ATTENTION] Ne changez ce paramètre qu'en cas de nécessité. (Défaut = 55111)": "[ATTENTION] Only change this setting if necessary. (Default = 55111)" }, "plugins\/ttscast\/plugin_info\/install.php": { diff --git a/core/i18n/en_US.json b/core/i18n/en_US.json index 06775849..14018674 100644 --- a/core/i18n/en_US.json +++ b/core/i18n/en_US.json @@ -178,12 +178,14 @@ "Visible": "Visible" }, "plugins\/ttscast\/plugin_info\/configuration.php": { + "Active le moteur de synthèse vocale Gemini TTS. Utilise la clé API Google Gemini ou le compte OAuth2 configuré ci-dessus.": "Active the Gemini TTS text-to-speech engine. Uses the Google Gemini API key or the OAuth2 account configured above.", + "Activer Gemini TTS": "Enable Gemini TTS", "Activer IA Générative": "Enable Generative AI", "Affiche le prompt système utilisé par l'IA lorsqu'aucun prompt personnalisé n'est défini. Le démon doit être démarré.": "Displays the system prompt used by the AI when no custom prompt is defined. The daemon must be running.", "Afficher": "Display", "Ajouter Clé (JSON)": "Add Key (JSON)", "Ajouter des Custom Radios (.json)": "Add Custom Radios (.json)", - "Ajouter un Custom Sound (.mp3)": "Add Custom Sound (.mp3)", + "Ajouter un Custom Sound": "Add a Custom Sound", "Ajouter un fichier :: Custom Radios": "Add file :: Custom Radios", "Ajouter un fichier :: Custom Sound": "Add file :: Custom Sound", "Allemand (de-DE)": "Deutsch (de-DE)", @@ -191,7 +193,7 @@ "Authentification IA": "AI authentication", "Autoriser l'usage de l'IA pour la reformulation des réponses. Désactivez cette option si vous ne souhaitez pas utiliser l'IA": "Allow the use of AI for response rephrasing. Disable this option if you do not want to use AI", "Ce paramètre est utilisé uniquement pour le test de synthèse vocale (TTS)": "This setting is used only for speech synthesis (TTS) testing", - "Ce paramètre permet de tester la reformulation des réponses à l'aide de l'IA.": "This setting allows you to test the reformulation of responses using AI.", + "Ce paramètre permet de tester la reformulation du texte à l'aide de l'IA avant la synthèse vocale.": "This setting allows you to test the rephrasing of the text using AI before speech synthesis.", "Clé API": "API key", "Clé API (Google AI Studio)": "API key (Google AI Studio)", "Clé API (Voice RSS)": "API Key (Voice RSS)", @@ -199,6 +201,8 @@ "Cocher cette case désactivera les messages de mise à jour du plugin dans le centre de message": "Checking this box will disable plugin update messages in the message center.", "Contournement du bug Google TTS de prononciation des mots contenant une apostrophe suivi de caractères accentués": "Workaround for Google TTS bug affecting pronunciation of words containing an apostrophe followed by accented characters", "Convertir les SingleQuote en DoubleQuote": "Convert SingleQuotes to DoubleQuotes", + "Critical": "Critical", + "Debug": "Debug", "Demande de génération du TTS de test envoyée (voir les logs du démon pour le résultat)": "Request to generate test TTS :: Sent (See daemon logs for result)", "Demande de mise à jour des listes :: Custom Sounds :: envoyée (voir les logs ttscast)": "Request to update lists :: Custom Sounds :: Sent (See ttscast logs for result)", "Demande de mise à jour des listes :: CustomRadios :: envoyée (voir les logs ttscast)": "Request to update lists :: CustomRadios :: Sent (See ttscast logs for result)", @@ -215,9 +219,11 @@ "Effacer Clé": "Delete Key", "Entrez votre ID de projet Google pour l'authentification avec le moteur d'IA.": "Enter your Google project ID for authentication with the AI engine.", "Entrez votre clé API pour l'authentification avec le moteur d'IA.": "Enter your API key for authentication with the AI engine.", + "Error": "Error", "Espagnol (es-ES)": "Spanish (es-ES)", "Ex: Ceci est un message de test pour la synthèse vocale à partir de Jeedom.": "Ex: This is a test message for speech synthesis from Jeedom.", "Ex: Nest Salon": "Ex: Nest Living Room", + "Ex: Parle d'une voix chaleureuse et rassurante.": "Ex: Speak in a warm and reassuring voice.", "Ex: Répond à la question s'il y en a une et reformule la phrase": "Ex: Responds to the question if there is one and rephrases the sentence", "Ex: enthousiaste et humoristique": "E.g., enthusiastic and humorous", "Facteur multiplicateur des cycles du démon (Défaut = x1)": "Daemon Cycle Multiplier Factor (Default = x1)", @@ -229,12 +235,19 @@ "Format d'encodage audio pour le moteur Google Cloud TTS (MP3 ou LINEAR16). LINEAR16 offre une meilleure qualité (WAV) mais les fichiers sont plus volumineux.": "Audio encoding format for the Google Cloud TTS engine (MP3 or LINEAR16). LINEAR16 offers better quality (WAV) but the files are larger.", "Français (fr-FR)": "French (fr-FR)", "Fréquence des cycles": "Cycles Frequency", + "Gemini 2.5": "Gemini 2.5", + "Gemini 3.x (Recommandé)": "Gemini 3.x (Recommended)", + "Gemini TTS - Voix multilingues": "Gemini TTS - Multilingual voices", "Google Cloud Text-To-Speech (Clé & Internet)": "Google Cloud Text-To-Speech (Key & Internet)", "Google Translate API (Internet)": "Google Translate API (Internet)", "Génère les fichiers TTS à chaque demande. Il est vivement recommandé de ne PAS cocher cette case, sauf pour faire des tests": "Generates TTS files on each request. It is strongly recommended that you do NOT check this box, except for testing purposes.", "Générer + Diffuser": "Generate + Broadcast", - "IA - Gemini": "AI - Gemini", + "IA & TTS - Gemini": "AI & TTS - Gemini", "Id Projet Google": "Google Project ID", + "Identique au démon (défaut)": "Same as the (default) daemon", + "Info": "Info", + "Instruction de style transmise au modèle Gemini TTS (ex: Parle d'une voix chaleureure et rassurante). Laissez vide pour le style par défaut.": "Style instruction sent to the Gemini TTS model (e.g., \"Speak in a warm and reassuring voice\"). Leave blank for the default style.", + "Instruction de style transmise au modèle Gemini TTS pour toutes les notifications (ex: Parle d'une voix chaleureuse et rassurante). Peut être surchargé par l'option style: dans les scénarios. Laissez vide pour le style neutre par défaut.": "Style instruction provided to the Gemini TTS model for all notifications (e.g., \"Speak in a warm and reassuring voice\"). Can be overridden by the style option in scenarios. Leave blank for the default neutral style.", "Italien (it-IT)": "Italian (it-IT)", "Jeedom TTS (Local)": "Jeedom TTS (Local)", "LINEAR16 (WAV - 24kHz)": "LINEAR16 (WAV - 24kHz)", @@ -276,7 +289,9 @@ "Mise à jour des listes :: Custom Sounds": "List Update :: Custom Sounds", "Mise à jour des listes :: Radios": "List Update :: Radios", "Mise à jour des listes :: Sounds": "List Update :: Sounds", - "Modèle IA": "IA model", + "Modèle Gemini TTS": "Gemini TTS Model", + "Modèle Gemini TTS à utiliser. Flash est plus économique, Pro est plus expressif.": "Use the Gemini TTS model. Flash is more economical; Pro is more expressive.", + "Modèle IA (Reformulation)": "AI Model (Rewrite)", "Modèles Stables (Recommandés)": "Stable Models (Recommended)", "Moteur TTS": "TTS Engine", "Moteur TTS à utiliser pour la synthèse vocale": "TTS engine to use for speech synthesis", @@ -285,6 +300,8 @@ "MàJ Radios": "Update Radios", "MàJ Sounds": "Update Sounds", "Ne PAS utiliser le cache": "Do NOT use cache", + "Niveau de log Chromecast": "Chromecast log level", + "Niveau de log des librairies Chromecast du démon. Par défaut, hérite du niveau de log global.": "Log level for the daemon's Chromecast libraries. By default, inherits the global log level.", "Nombre de jours": "Number of days", "Normal (0)": "Normal (0)", "Normal (1.0)": "Normal (1.0)", @@ -331,27 +348,37 @@ "SITE": "SITE", "Sauvegardez bien votre configuration AVANT d'utiliser le bouton [Générer + Diffuser]": "Be sure to save your configuration BEFORE using the [Generate + Broadcast] button", "Serbe (sr-RS)": "Serbian (sr-RS)", + "Si activé, la lecture audio démarre dès les premières syllabes générées, sans attendre la fin de la synthèse vocale. Réduit la latence perçue.": "If enabled, audio playback starts as soon as the first syllables are generated, without waiting for the text-to-speech synthesis to finish. Reduces perceived latency.", + "Si activé, toutes les notifications utilisent Gemini TTS par défaut.": "If enabled, all notifications will use Gemini TTS by default.", + "Si coché, le test utilise le mode streaming Gemini TTS (lecture directe sans fichier intermédiaire). Nécessite que Gemini TTS soit activé et que le paramètre 'Tester avec Gemini TTS' soit coché.": "If checked, the test uses Gemini TTS streaming mode (direct playback without an intermediate file). Requires that Gemini TTS be enabled and that the 'Test with Gemini TTS' setting be checked.", + "Si coché, le test utilise le moteur Gemini TTS au lieu du moteur par défaut. Nécessite que Gemini TTS soit activé.": "If checked, the test uses the Gemini TTS engine instead of the default engine. Requires that Gemini TTS be enabled.", + "Streaming Gemini TTS par défaut": "Default Gemini TTS streaming", + "Style de voix (Gemini TTS)": "Voice style (Gemini TTS)", + "Style par défaut (Gemini TTS)": "Default style (Gemini TTS)", "Sélectionnez le mode d'authentification à utiliser pour se connecter au moteur d'IA.": "Select the authentication mode to use to connect to the AI engine.", "Sélectionnez le modèle d'IA à utiliser pour la reformulation des réponses.": "Select the AI model to use for response rephrasing.", "TTS (Text To Speech)": "TTS (Text To Speech)", "Temps (entre 0 et 3600 sec) au bout duquel la commande est dans tous les cas exécutée, même si l'équipement Google est encore en cours de lecture (par défaut : 60sec)": "Time (between 0 and 3600 sec) after which the command is executed in all cases, even if the Google device is still playing (default: 60sec)", - "Tester avec l'IA": "Test with AI", + "Tester avec Gemini TTS": "Test with Gemini TTS", + "Tester avec la reformulation IA": "Test with AI rephrasing", "Tester avec la syntaxe SSML (TTS)": "Test with SSML syntax (TTS)", + "Tester avec le mode streaming": "Test using streaming mode", "Tester la Synthèse Vocale (TTS)": "Test speech synthesis (TTS)", "Tests": "Tests", "Timeout": "Timeout", "Timeout (entre 5 et 300 sec) d'attente de la génération du fichier TTS lors d'un appel via API (par défaut : 30sec)": "Timeout (between 5 and 300 sec) to wait for TTS file generation when calling via PLC (default: 30sec)", "Timeout de l'API 'GenerateTTS' (secondes)": "GenerateTTS API timeout (seconds)", - "Ton \/ Style": "Tone \/ Style", + "Ton de Reformulation": "Rewording", "URL Jeedom Externe": "Jeedom External URL", "Upload Clé API (OK) :: ": "Upload API Key (OK) :: ", "Upload Clé API :: Sauvegarde OK": "Upload API Key :: Save OK", "Upload Custom Radios (OK) :: ": "Upload Custom Radios (OK) :: ", "Upload Custom Sound (OK) :: ": "Upload Custom Sound (OK) :: ", "Upload d'un fichier (.json) pour mettre à jour la liste des Custom Radios": "Upload a file (.json) to update the list of Custom Radios", - "Upload un fichier (.mp3) pour l'ajouter au répertoire des Custom Sounds": "Upload a file (.mp3) to add it to the Custom Sounds directory", + "Upload un fichier (.mp3, .wav, .ogg, .opus, .flac) pour l'ajouter au répertoire des Custom Sounds": "Upload a file (.mp3, .wav, .ogg, .opus, .flac) to add it to the Custom Sounds directory", "Uploader votre clé JSON en utilisant le bouton 'Ajouter Clé (JSON)'": "Upload your JSON key using the 'Add Key (JSON)' button", "Utilise l'URL externe de Jeedom pour la lecture des fichiers TTS plutôt que l'URL interne (Recommandé = décoché)": "Use the Jeedom external URL for reading TTS files rather than the internal URL (Recommended = unchecked)", + "Utiliser Gemini TTS par défaut": "Use Gemini TTS by default", "Utiliser la Reformulation IA par défaut": "Use AI rephrasing by default", "Valeur par défaut = Normal (0)": "Default Value = Normal (0)", "Valeur par défaut = Normal (1.0)": "Default Value = Normal (1.0)", @@ -367,6 +394,9 @@ "Vitesse de Dictée (Voice RSS TTS)": "Dictation Speed (Voice RSS TTS)", "Vitesse de Dictée (gCloud TTS)": "Dictation Speed (gCloud TTS)", "Voice RSS API (Clé & Internet)": "Voice RSS API (Key & Internet)", + "Voix Gemini TTS": "Gemini TTS Voice", + "Voix utilisée par défaut. Toutes les voix Gemini TTS sont multilingues (détection automatique de la langue).": "Default voice. All Gemini TTS voices are multilingual (automatic language detection).", + "Warning": "Warning", "[ATTENTION] Ne changez ce paramètre qu'en cas de nécessité. (Défaut = 55111)": "[ATTENTION] Only change this setting if necessary. (Default = 55111)" }, "plugins\/ttscast\/plugin_info\/install.php": { diff --git a/core/i18n/es_ES.json b/core/i18n/es_ES.json index ca76a730..ef86d59e 100644 --- a/core/i18n/es_ES.json +++ b/core/i18n/es_ES.json @@ -178,12 +178,14 @@ "Visible": "Visible" }, "plugins\/ttscast\/plugin_info\/configuration.php": { + "Active le moteur de synthèse vocale Gemini TTS. Utilise la clé API Google Gemini ou le compte OAuth2 configuré ci-dessus.": "Activa el motor de síntesis de voz Gemini TTS. Utiliza la clave API de Google Gemini o la cuenta OAuth2 configurada anteriormente.", + "Activer Gemini TTS": "Activar Gemini TTS", "Activer IA Générative": "Activar IA generativa", "Affiche le prompt système utilisé par l'IA lorsqu'aucun prompt personnalisé n'est défini. Le démon doit être démarré.": "Muestra el mensaje del sistema utilizado por la IA cuando no se ha definido ningún mensaje personalizado. El demonio debe estar en ejecución.", "Afficher": "Display", "Ajouter Clé (JSON)": "Add Key (JSON)", "Ajouter des Custom Radios (.json)": "Add Custom Radios (.json)", - "Ajouter un Custom Sound (.mp3)": "Add Custom Sound (.mp3)", + "Ajouter un Custom Sound": "Añadir un sonido personalizado", "Ajouter un fichier :: Custom Radios": "Add file :: Custom Radios", "Ajouter un fichier :: Custom Sound": "Add file :: Custom Sound", "Allemand (de-DE)": "Deutsch (de-DE)", @@ -191,7 +193,7 @@ "Authentification IA": "Autenticación IA", "Autoriser l'usage de l'IA pour la reformulation des réponses. Désactivez cette option si vous ne souhaitez pas utiliser l'IA": "Permitir el uso de IA para reformular respuestas. Desactiva esta opción si no deseas utilizar IA", "Ce paramètre est utilisé uniquement pour le test de synthèse vocale (TTS)": "This setting is used only for speech synthesis (TTS) testing", - "Ce paramètre permet de tester la reformulation des réponses à l'aide de l'IA.": "Este parámetro permite probar la reformulación de las respuestas mediante IA.", + "Ce paramètre permet de tester la reformulation du texte à l'aide de l'IA avant la synthèse vocale.": "Esta opción permite probar la reformulación del texto mediante IA antes de la síntesis de voz.", "Clé API": "Clave API", "Clé API (Google AI Studio)": "Clave API (Google AI Studio)", "Clé API (Voice RSS)": "API Key (Voice RSS)", @@ -199,6 +201,8 @@ "Cocher cette case désactivera les messages de mise à jour du plugin dans le centre de message": "Al marcar esta casilla, se desactivarán los mensajes de actualización del complemento en el centro de mensajes.", "Contournement du bug Google TTS de prononciation des mots contenant une apostrophe suivi de caractères accentués": "Solución al error de Google TTS en la pronunciación de palabras que contienen un apóstrofo seguido de caracteres acentuados", "Convertir les SingleQuote en DoubleQuote": "Convertir las comillas simples en comillas dobles", + "Critical": "Crítico", + "Debug": "Depuración", "Demande de génération du TTS de test envoyée (voir les logs du démon pour le résultat)": "Request to generate test TTS :: Sent (See daemon logs for result)", "Demande de mise à jour des listes :: Custom Sounds :: envoyée (voir les logs ttscast)": "Request to update lists :: Custom Sounds :: Sent (See ttscast logs for result)", "Demande de mise à jour des listes :: CustomRadios :: envoyée (voir les logs ttscast)": "Request to update lists :: CustomRadios :: Sent (See ttscast logs for result)", @@ -215,9 +219,11 @@ "Effacer Clé": "Delete Key", "Entrez votre ID de projet Google pour l'authentification avec le moteur d'IA.": "Introduzca su ID de proyecto de Google para autenticarse en el motor de IA.", "Entrez votre clé API pour l'authentification avec le moteur d'IA.": "Introduzca su clave API para autenticarse en el motor de IA.", + "Error": "Error", "Espagnol (es-ES)": "Spanish (es-ES)", "Ex: Ceci est un message de test pour la synthèse vocale à partir de Jeedom.": "Ex: This is a test message for speech synthesis from Jeedom.", "Ex: Nest Salon": "Ex: Nest Living Room", + "Ex: Parle d'une voix chaleureuse et rassurante.": "Ej.: Habla con una voz cálida y tranquilizadora.", "Ex: Répond à la question s'il y en a une et reformule la phrase": "Ejemplo: Responde a la pregunta si hay alguna y reformula la frase", "Ex: enthousiaste et humoristique": "Por ejemplo: entusiasta y con sentido del humor", "Facteur multiplicateur des cycles du démon (Défaut = x1)": "Daemon Cycle Multiplier Factor (Default = x1)", @@ -229,12 +235,19 @@ "Format d'encodage audio pour le moteur Google Cloud TTS (MP3 ou LINEAR16). LINEAR16 offre une meilleure qualité (WAV) mais les fichiers sont plus volumineux.": "Formato de codificación de audio para el motor Google Cloud TTS (MP3 o LINEAR16). LINEAR16 ofrece una mejor calidad (WAV), pero los archivos son más pesados.", "Français (fr-FR)": "French (fr-FR)", "Fréquence des cycles": "Cycles Frequency", + "Gemini 2.5": "Gemini 2.5", + "Gemini 3.x (Recommandé)": "Gemini 3.x (Recomendado)", + "Gemini TTS - Voix multilingues": "Gemini TTS - Voces multilingües", "Google Cloud Text-To-Speech (Clé & Internet)": "Google Cloud Text-To-Speech (Key & Internet)", "Google Translate API (Internet)": "Google Translate API (Internet)", "Génère les fichiers TTS à chaque demande. Il est vivement recommandé de ne PAS cocher cette case, sauf pour faire des tests": "Generates TTS files on each request. It is strongly recommended that you do NOT check this box, except for testing purposes.", "Générer + Diffuser": "Generate + Broadcast", - "IA - Gemini": "IA - Gemini", + "IA & TTS - Gemini": "IA y TTS - Gemini", "Id Projet Google": "Proyecto Id Google", + "Identique au démon (défaut)": "Igual que el demonio (por defecto)", + "Info": "Información", + "Instruction de style transmise au modèle Gemini TTS (ex: Parle d'une voix chaleureure et rassurante). Laissez vide pour le style par défaut.": "Instrucción de estilo transmitida al modelo Gemini TTS (por ejemplo: «Habla con una voz cálida y tranquilizadora»). Déjalo en blanco para utilizar el estilo predeterminado.", + "Instruction de style transmise au modèle Gemini TTS pour toutes les notifications (ex: Parle d'une voix chaleureuse et rassurante). Peut être surchargé par l'option style: dans les scénarios. Laissez vide pour le style neutre par défaut.": "Instrucción de estilo transmitida al modelo Gemini TTS para todas las notificaciones (por ejemplo: «Habla con una voz cálida y tranquilizadora»). Se puede anular mediante la opción «style:» en los escenarios. Déjelo en blanco para el estilo neutro predeterminado.", "Italien (it-IT)": "Italian (it-IT)", "Jeedom TTS (Local)": "Jeedom TTS (Local)", "LINEAR16 (WAV - 24kHz)": "LINEAR16 (WAV - 24 kHz)", @@ -276,7 +289,9 @@ "Mise à jour des listes :: Custom Sounds": "List Update :: Custom Sounds", "Mise à jour des listes :: Radios": "List Update :: Radios", "Mise à jour des listes :: Sounds": "List Update :: Sounds", - "Modèle IA": "Modelo IA", + "Modèle Gemini TTS": "Modelo Gemini TTS", + "Modèle Gemini TTS à utiliser. Flash est plus économique, Pro est plus expressif.": "Modelo Gemini TTS a utilizar. Flash es más económico, Pro es más expresivo.", + "Modèle IA (Reformulation)": "Modelo IA (Reformulación)", "Modèles Stables (Recommandés)": "Modelos estables (recomendados)", "Moteur TTS": "TTS Engine", "Moteur TTS à utiliser pour la synthèse vocale": "TTS engine to use for speech synthesis", @@ -285,6 +300,8 @@ "MàJ Radios": "Update Radios", "MàJ Sounds": "Update Sounds", "Ne PAS utiliser le cache": "Do NOT use cache", + "Niveau de log Chromecast": "Registro de Chromecast", + "Niveau de log des librairies Chromecast du démon. Par défaut, hérite du niveau de log global.": "Nivel de registro de las bibliotecas Chromecast del demonio. Por defecto, hereda el nivel de registro global.", "Nombre de jours": "Number of days", "Normal (0)": "Normal (0)", "Normal (1.0)": "Normal (1.0)", @@ -331,27 +348,37 @@ "SITE": "SITE", "Sauvegardez bien votre configuration AVANT d'utiliser le bouton [Générer + Diffuser]": "Guarde bien su configuración ANTES de utilizar el botón [Generar + Difundir]", "Serbe (sr-RS)": "Serbian (sr-RS)", + "Si activé, la lecture audio démarre dès les premières syllabes générées, sans attendre la fin de la synthèse vocale. Réduit la latence perçue.": "Si está activada, la reproducción de audio comienza desde las primeras sílabas generadas, sin esperar a que finalice la síntesis de voz. Reduce la latencia percibida.", + "Si activé, toutes les notifications utilisent Gemini TTS par défaut.": "Si está activada, todas las notificaciones utilizarán Gemini TTS de forma predeterminada.", + "Si coché, le test utilise le mode streaming Gemini TTS (lecture directe sans fichier intermédiaire). Nécessite que Gemini TTS soit activé et que le paramètre 'Tester avec Gemini TTS' soit coché.": "Si se marca esta casilla, la prueba utiliza el modo de streaming de Gemini TTS (reproducción directa sin archivo intermedio). Requiere que Gemini TTS esté activado y que la opción «Probar con Gemini TTS» esté marcada.", + "Si coché, le test utilise le moteur Gemini TTS au lieu du moteur par défaut. Nécessite que Gemini TTS soit activé.": "Si se marca esta casilla, la prueba utilizará el motor Gemini TTS en lugar del motor predeterminado. Requiere que Gemini TTS esté activado.", + "Streaming Gemini TTS par défaut": "Transmisión de Gemini TTS por defecto", + "Style de voix (Gemini TTS)": "Estilo de voz (Gemini TTS)", + "Style par défaut (Gemini TTS)": "Estilo predeterminado (Gemini TTS)", "Sélectionnez le mode d'authentification à utiliser pour se connecter au moteur d'IA.": "Seleccione el modo de autenticación que se utilizará para conectarse al motor de IA.", "Sélectionnez le modèle d'IA à utiliser pour la reformulation des réponses.": "Seleccione el modelo de IA que se utilizará para reformular las respuestas.", "TTS (Text To Speech)": "TTS (Text To Speech)", "Temps (entre 0 et 3600 sec) au bout duquel la commande est dans tous les cas exécutée, même si l'équipement Google est encore en cours de lecture (par défaut : 60sec)": "Tiempo (entre 0 y 3600 seg) tras el cual se ejecuta el comando en todos los casos, aunque se siga leyendo el dispositivo Google (por defecto: 60seg)", - "Tester avec l'IA": "Probar con IA", + "Tester avec Gemini TTS": "Probar con Gemini TTS", + "Tester avec la reformulation IA": "Probar con la reformulación de IA", "Tester avec la syntaxe SSML (TTS)": "Test with SSML syntax (TTS)", + "Tester avec le mode streaming": "Probar con el modo de streaming", "Tester la Synthèse Vocale (TTS)": "Test speech synthesis (TTS)", "Tests": "Tests", "Timeout": "Tiempo de espera", "Timeout (entre 5 et 300 sec) d'attente de la génération du fichier TTS lors d'un appel via API (par défaut : 30sec)": "Tiempo de espera (entre 5 y 300 s) para la generación del archivo TTS durante una llamada a través del PLC (por defecto: 30 s)", "Timeout de l'API 'GenerateTTS' (secondes)": "Tiempo de espera de la API GenerateTTS (segundos)", - "Ton \/ Style": "Tono \/ Estilo", + "Ton de Reformulation": "Tono de reformulación", "URL Jeedom Externe": "Jeedom External URL", "Upload Clé API (OK) :: ": "Upload API Key (OK) :: ", "Upload Clé API :: Sauvegarde OK": "Upload API Key :: Save OK", "Upload Custom Radios (OK) :: ": "Upload Custom Radios (OK) :: ", "Upload Custom Sound (OK) :: ": "Upload Custom Sound (OK) :: ", "Upload d'un fichier (.json) pour mettre à jour la liste des Custom Radios": "Upload a file (.json) to update the list of Custom Radios", - "Upload un fichier (.mp3) pour l'ajouter au répertoire des Custom Sounds": "Upload a file (.mp3) to add it to the Custom Sounds directory", + "Upload un fichier (.mp3, .wav, .ogg, .opus, .flac) pour l'ajouter au répertoire des Custom Sounds": "Sube un archivo (.mp3, .wav, .ogg, .opus, .flac) para añadirlo al directorio de sonidos personalizados", "Uploader votre clé JSON en utilisant le bouton 'Ajouter Clé (JSON)'": "Upload your JSON key using the 'Add Key (JSON)' button", "Utilise l'URL externe de Jeedom pour la lecture des fichiers TTS plutôt que l'URL interne (Recommandé = décoché)": "Use the Jeedom external URL for reading TTS files rather than the internal URL (Recommended = unchecked)", + "Utiliser Gemini TTS par défaut": "Usar Gemini TTS como predeterminado", "Utiliser la Reformulation IA par défaut": "Usar la reformulación de IA por defecto", "Valeur par défaut = Normal (0)": "Default Value = Normal (0)", "Valeur par défaut = Normal (1.0)": "Default Value = Normal (1.0)", @@ -367,6 +394,9 @@ "Vitesse de Dictée (Voice RSS TTS)": "Dictation Speed (Voice RSS TTS)", "Vitesse de Dictée (gCloud TTS)": "Dictation Speed (gCloud TTS)", "Voice RSS API (Clé & Internet)": "Voice RSS API (Key & Internet)", + "Voix Gemini TTS": "Voz Gemini TTS", + "Voix utilisée par défaut. Toutes les voix Gemini TTS sont multilingues (détection automatique de la langue).": "Voz predeterminada. Todas las voces de Gemini TTS son multilingües (detección automática del idioma).", + "Warning": "Advertencia", "[ATTENTION] Ne changez ce paramètre qu'en cas de nécessité. (Défaut = 55111)": "[ATTENTION] Only change this setting if necessary. (Default = 55111)" }, "plugins\/ttscast\/plugin_info\/install.php": { diff --git a/core/i18n/it_IT.json b/core/i18n/it_IT.json index 7570f4fc..6c6c931d 100644 --- a/core/i18n/it_IT.json +++ b/core/i18n/it_IT.json @@ -178,12 +178,14 @@ "Visible": "Visibile" }, "plugins\/ttscast\/plugin_info\/configuration.php": { + "Active le moteur de synthèse vocale Gemini TTS. Utilise la clé API Google Gemini ou le compte OAuth2 configuré ci-dessus.": "Attiva il motore di sintesi vocale Gemini TTS. Utilizza la chiave API di Google Gemini o l'account OAuth2 configurato sopra.", + "Activer Gemini TTS": "Attiva Gemini TTS", "Activer IA Générative": "Attiva IA generativa", "Affiche le prompt système utilisé par l'IA lorsqu'aucun prompt personnalisé n'est défini. Le démon doit être démarré.": "Visualizza il prompt di sistema utilizzato dall'IA quando non è definito alcun prompt personalizzato. Il demone deve essere avviato.", "Afficher": "Vista", "Ajouter Clé (JSON)": "Aggiungi chiave (JSON)", "Ajouter des Custom Radios (.json)": "Aggiungere radio personalizzate (.json)", - "Ajouter un Custom Sound (.mp3)": "Aggiungere un suono personalizzato (.mp3)", + "Ajouter un Custom Sound": "Aggiungi un suono personalizzato", "Ajouter un fichier :: Custom Radios": "Aggiungere un file :: Radio personalizzate", "Ajouter un fichier :: Custom Sound": "Aggiungere un file :: Suono personalizzato", "Allemand (de-DE)": "Tedesco (de-DE)", @@ -191,7 +193,7 @@ "Authentification IA": "Autenticazione IA", "Autoriser l'usage de l'IA pour la reformulation des réponses. Désactivez cette option si vous ne souhaitez pas utiliser l'IA": "Consenti l'uso dell'IA per la riformulazione delle risposte. Disattiva questa opzione se non desideri utilizzare l'IA", "Ce paramètre est utilisé uniquement pour le test de synthèse vocale (TTS)": "Questo parametro viene utilizzato solo per il test Text-to-Speech (TTS)", - "Ce paramètre permet de tester la reformulation des réponses à l'aide de l'IA.": "Questo parametro consente di testare la riformulazione delle risposte utilizzando l'IA.", + "Ce paramètre permet de tester la reformulation du texte à l'aide de l'IA avant la synthèse vocale.": "Questa impostazione consente di testare la riformulazione del testo tramite l'IA prima della sintesi vocale.", "Clé API": "Chiave API", "Clé API (Google AI Studio)": "Chiave API (Google AI Studio)", "Clé API (Voice RSS)": "Chiave API (RSS vocale)", @@ -199,6 +201,8 @@ "Cocher cette case désactivera les messages de mise à jour du plugin dans le centre de message": "Selezionando questa casella, i messaggi di aggiornamento del plugin nel centro messaggi saranno disattivati.", "Contournement du bug Google TTS de prononciation des mots contenant une apostrophe suivi de caractères accentués": "Come risolvere il bug di Google TTS nella pronuncia delle parole contenenti un apostrofo seguito da caratteri accentati", "Convertir les SingleQuote en DoubleQuote": "Convertire le virgolette singole in virgolette doppie", + "Critical": "Critico", + "Debug": "Debug", "Demande de génération du TTS de test envoyée (voir les logs du démon pour le résultat)": "Richiesta di generazione TTS di prova inviata (vedere i log del demone per i risultati)", "Demande de mise à jour des listes :: Custom Sounds :: envoyée (voir les logs ttscast)": "Richiesta di aggiornamento delle liste :: Suoni personalizzati :: inviata (vedi log ttscast)", "Demande de mise à jour des listes :: CustomRadios :: envoyée (voir les logs ttscast)": "Richiesta di aggiornamento della lista :: CustomRadios :: inviata (vedi log ttscast)", @@ -215,9 +219,11 @@ "Effacer Clé": "Tasto di cancellazione", "Entrez votre ID de projet Google pour l'authentification avec le moteur d'IA.": "Inserisci il tuo ID progetto Google per l'autenticazione con il motore IA.", "Entrez votre clé API pour l'authentification avec le moteur d'IA.": "Inserisci la tua chiave API per l'autenticazione con il motore IA.", + "Error": "Errore", "Espagnol (es-ES)": "Spagnolo (es-ES)", "Ex: Ceci est un message de test pour la synthèse vocale à partir de Jeedom.": "Es: Questo è un messaggio di prova per la sintesi vocale di Jeedom.", "Ex: Nest Salon": "Es: Salone Nest", + "Ex: Parle d'une voix chaleureuse et rassurante.": "Es.: Parla con voce calorosa e rassicurante.", "Ex: Répond à la question s'il y en a une et reformule la phrase": "Esempio: risponde alla domanda se ce n'è una e riformula la frase", "Ex: enthousiaste et humoristique": "Es.: entusiasta e spiritoso", "Facteur multiplicateur des cycles du démon (Défaut = x1)": "Fattore di moltiplicazione dei cicli demonici (default = x1)", @@ -229,12 +235,19 @@ "Format d'encodage audio pour le moteur Google Cloud TTS (MP3 ou LINEAR16). LINEAR16 offre une meilleure qualité (WAV) mais les fichiers sont plus volumineux.": "Formato di codifica audio per il motore Google Cloud TTS (MP3 o LINEAR16). LINEAR16 offre una qualità migliore (WAV), ma i file sono più voluminosi.", "Français (fr-FR)": "Francese (fr-FR)", "Fréquence des cycles": "Frequenza del ciclo", + "Gemini 2.5": "Gemini 2.5", + "Gemini 3.x (Recommandé)": "Gemini 3.x (consigliato)", + "Gemini TTS - Voix multilingues": "Gemini TTS - Voci multilingue", "Google Cloud Text-To-Speech (Clé & Internet)": "Sintesi vocale di Google Cloud (Chiave e Internet)", "Google Translate API (Internet)": "API di Google Translate (Internet)", "Génère les fichiers TTS à chaque demande. Il est vivement recommandé de ne PAS cocher cette case, sauf pour faire des tests": "Genera file TTS ad ogni richiesta. Si raccomanda vivamente di NON selezionare questa casella, se non a scopo di test", "Générer + Diffuser": "Generare + Trasmettere", - "IA - Gemini": "IA - Gemini", + "IA & TTS - Gemini": "IA e TTS - Gemini", "Id Projet Google": "Progetto Google ID", + "Identique au démon (défaut)": "Identico al demone (predefinito)", + "Info": "Informazioni", + "Instruction de style transmise au modèle Gemini TTS (ex: Parle d'une voix chaleureure et rassurante). Laissez vide pour le style par défaut.": "Istruzione di stile trasmessa al modello Gemini TTS (es.: Parla con voce calorosa e rassicurante). Lasciare vuoto per lo stile predefinito.", + "Instruction de style transmise au modèle Gemini TTS pour toutes les notifications (ex: Parle d'une voix chaleureuse et rassurante). Peut être surchargé par l'option style: dans les scénarios. Laissez vide pour le style neutre par défaut.": "Istruzione di stile trasmessa al modello Gemini TTS per tutte le notifiche (es.: Parla con voce calorosa e rassicurante). Può essere sovrascritta dall'opzione style: negli scenari. Lasciare vuoto per lo stile neutro predefinito.", "Italien (it-IT)": "Italiano (it-IT)", "Jeedom TTS (Local)": "Jeedom TTS (locale)", "LINEAR16 (WAV - 24kHz)": "LINEAR16 (WAV - 24kHz)", @@ -276,7 +289,9 @@ "Mise à jour des listes :: Custom Sounds": "Aggiornamenti elenco :: Suoni personalizzati", "Mise à jour des listes :: Radios": "Aggiornamenti dell'elenco :: Radio", "Mise à jour des listes :: Sounds": "Aggiornamenti della lista :: Suoni", - "Modèle IA": "Modello IA", + "Modèle Gemini TTS": "Modello Gemini TTS", + "Modèle Gemini TTS à utiliser. Flash est plus économique, Pro est plus expressif.": "Modello Gemini TTS da utilizzare. Flash è più economico, Pro è più espressivo.", + "Modèle IA (Reformulation)": "Modello IA (Riformulazione)", "Modèles Stables (Recommandés)": "Modelli stabili (consigliati)", "Moteur TTS": "Motore TTS", "Moteur TTS à utiliser pour la synthèse vocale": "Motore TTS per la sintesi vocale", @@ -285,6 +300,8 @@ "MàJ Radios": "Aggiornamenti radiofonici", "MàJ Sounds": "Aggiornamento dei suoni", "Ne PAS utiliser le cache": "NON utilizzare la cache", + "Niveau de log Chromecast": "Livello di log Chromecast", + "Niveau de log des librairies Chromecast du démon. Par défaut, hérite du niveau de log global.": "Livello di log delle librerie Chromecast del demone. Per impostazione predefinita, eredita il livello di log globale.", "Nombre de jours": "Numero di giorni", "Normal (0)": "Normale (0)", "Normal (1.0)": "Normale (1.0)", @@ -331,27 +348,37 @@ "SITE": "SITO", "Sauvegardez bien votre configuration AVANT d'utiliser le bouton [Générer + Diffuser]": "Salvare la configurazione PRIMA di utilizzare il pulsante [Genera + Trasmetti]", "Serbe (sr-RS)": "Serbo (sr-RS)", + "Si activé, la lecture audio démarre dès les premières syllabes générées, sans attendre la fin de la synthèse vocale. Réduit la latence perçue.": "Se attivata, la riproduzione audio inizia non appena vengono generate le prime sillabe, senza attendere la fine della sintesi vocale. Riduce la latenza percepita.", + "Si activé, toutes les notifications utilisent Gemini TTS par défaut.": "Se attivata, tutte le notifiche utilizzano Gemini TTS come impostazione predefinita.", + "Si coché, le test utilise le mode streaming Gemini TTS (lecture directe sans fichier intermédiaire). Nécessite que Gemini TTS soit activé et que le paramètre 'Tester avec Gemini TTS' soit coché.": "Se selezionato, il test utilizza la modalità di streaming Gemini TTS (riproduzione diretta senza file intermedio). Richiede che Gemini TTS sia attivato e che l'opzione 'Testa con Gemini TTS' sia selezionata.", + "Si coché, le test utilise le moteur Gemini TTS au lieu du moteur par défaut. Nécessite que Gemini TTS soit activé.": "Se selezionato, il test utilizza il motore Gemini TTS invece del motore predefinito. Richiede che Gemini TTS sia attivato.", + "Streaming Gemini TTS par défaut": "Streaming Gemini TTS predefinito", + "Style de voix (Gemini TTS)": "Stile vocale (Gemini TTS)", + "Style par défaut (Gemini TTS)": "Stile predefinito (Gemini TTS)", "Sélectionnez le mode d'authentification à utiliser pour se connecter au moteur d'IA.": "Selezionare la modalità di autenticazione da utilizzare per connettersi al motore IA.", "Sélectionnez le modèle d'IA à utiliser pour la reformulation des réponses.": "Seleziona il modello di IA da utilizzare per la riformulazione delle risposte.", "TTS (Text To Speech)": "TTS (Text To Speech)", "Temps (entre 0 et 3600 sec) au bout duquel la commande est dans tous les cas exécutée, même si l'équipement Google est encore en cours de lecture (par défaut : 60sec)": "Tempo (compreso tra 0 e 3600 sec) dopo il quale il comando viene eseguito in ogni caso, anche se il dispositivo Google è ancora in lettura (default: 60sec)", - "Tester avec l'IA": "Prova con l'IA", + "Tester avec Gemini TTS": "Prova con Gemini TTS", + "Tester avec la reformulation IA": "Prova con la riformulazione IA", "Tester avec la syntaxe SSML (TTS)": "Test con la sintassi SSML (TTS)", + "Tester avec le mode streaming": "Prova con la modalità streaming", "Tester la Synthèse Vocale (TTS)": "Sintesi vocale di prova (TTS)", "Tests": "Test", "Timeout": "Timeout", "Timeout (entre 5 et 300 sec) d'attente de la génération du fichier TTS lors d'un appel via API (par défaut : 30sec)": "Timeout (tra 5 e 300 sec) per attendere la generazione del file TTS durante una chiamata via PLC (default: 30sec)", "Timeout de l'API 'GenerateTTS' (secondes)": "Timeout API GenerateTTS (secondi)", - "Ton \/ Style": "Tono \/ Stile", + "Ton de Reformulation": "Tono di riformulazione", "URL Jeedom Externe": "URL esterno di Jeedom", "Upload Clé API (OK) :: ": "Caricare la chiave API (OK) ::", "Upload Clé API :: Sauvegarde OK": "Carica chiave API :: Salva OK", "Upload Custom Radios (OK) :: ": "Caricare le radio personalizzate (OK) ::", "Upload Custom Sound (OK) :: ": "Carica suono personalizzato (OK) ::", "Upload d'un fichier (.json) pour mettre à jour la liste des Custom Radios": "Caricare un file (.json) per aggiornare l'elenco delle radio personalizzate", - "Upload un fichier (.mp3) pour l'ajouter au répertoire des Custom Sounds": "Caricare un file (.mp3) per aggiungerlo alla cartella Custom Sounds (Suoni personalizzati)", + "Upload un fichier (.mp3, .wav, .ogg, .opus, .flac) pour l'ajouter au répertoire des Custom Sounds": "Carica un file (.mp3, .wav, .ogg, .opus, .flac) per aggiungerlo alla libreria dei suoni personalizzati", "Uploader votre clé JSON en utilisant le bouton 'Ajouter Clé (JSON)'": "Caricare la chiave JSON utilizzando il pulsante \"Aggiungi chiave (JSON)\"", "Utilise l'URL externe de Jeedom pour la lecture des fichiers TTS plutôt que l'URL interne (Recommandé = décoché)": "Utilizzare l'URL esterno di Jeedom per leggere i file TTS anziché l'URL interno (consigliato = deselezionato)", + "Utiliser Gemini TTS par défaut": "Utilizza Gemini TTS come impostazione predefinita", "Utiliser la Reformulation IA par défaut": "Utilizzare la riformulazione IA predefinita", "Valeur par défaut = Normal (0)": "Valore predefinito = Normale (0)", "Valeur par défaut = Normal (1.0)": "Valore predefinito = Normale (1.0)", @@ -367,6 +394,9 @@ "Vitesse de Dictée (Voice RSS TTS)": "Velocità di dettatura (Voice RSS TTS)", "Vitesse de Dictée (gCloud TTS)": "Velocità di dettatura (gCloud TTS)", "Voice RSS API (Clé & Internet)": "API RSS vocale (chiave e Internet)", + "Voix Gemini TTS": "Voce Gemini TTS", + "Voix utilisée par défaut. Toutes les voix Gemini TTS sont multilingues (détection automatique de la langue).": "Voce utilizzata di default. Tutte le voci Gemini TTS sono multilingue (rilevamento automatico della lingua).", + "Warning": "Avviso", "[ATTENTION] Ne changez ce paramètre qu'en cas de nécessité. (Défaut = 55111)": "[Questo parametro può essere modificato solo se necessario. (Predefinito = 55111)" }, "plugins\/ttscast\/plugin_info\/install.php": { diff --git a/core/i18n/pt_PT.json b/core/i18n/pt_PT.json index 2388ee2d..d8bfb46d 100644 --- a/core/i18n/pt_PT.json +++ b/core/i18n/pt_PT.json @@ -178,12 +178,14 @@ "Visible": "Visível" }, "plugins\/ttscast\/plugin_info\/configuration.php": { + "Active le moteur de synthèse vocale Gemini TTS. Utilise la clé API Google Gemini ou le compte OAuth2 configuré ci-dessus.": "Ativa o motor de síntese de voz Gemini TTS. Utiliza a chave API do Google Gemini ou a conta OAuth2 configurada acima.", + "Activer Gemini TTS": "Ativar o Gemini TTS", "Activer IA Générative": "Ativar IA Gerativa", "Affiche le prompt système utilisé par l'IA lorsqu'aucun prompt personnalisé n'est défini. Le démon doit être démarré.": "Exibe o prompt do sistema utilizado pela IA quando não está definido nenhum prompt personalizado. O daemon deve ser iniciado.", "Afficher": "Ver", "Ajouter Clé (JSON)": "Adicionar chave (JSON)", "Ajouter des Custom Radios (.json)": "Adicionar rádios personalizados (.json)", - "Ajouter un Custom Sound (.mp3)": "Adicionar um som personalizado (.mp3)", + "Ajouter un Custom Sound": "Adicionar um som personalizado", "Ajouter un fichier :: Custom Radios": "Adicionar um ficheiro :: Rádios personalizados", "Ajouter un fichier :: Custom Sound": "Adicionar um ficheiro :: Som personalizado", "Allemand (de-DE)": "Alemão (de-DE)", @@ -191,7 +193,7 @@ "Authentification IA": "Autenticação IA", "Autoriser l'usage de l'IA pour la reformulation des réponses. Désactivez cette option si vous ne souhaitez pas utiliser l'IA": "Autorizar o uso de IA para reformular respostas. Desative esta opção se não quiser usar IA", "Ce paramètre est utilisé uniquement pour le test de synthèse vocale (TTS)": "Este parâmetro só é utilizado para o teste de conversão de texto em voz (TTS)", - "Ce paramètre permet de tester la reformulation des réponses à l'aide de l'IA.": "Esta configuração permite testar a reformulação das respostas usando IA.", + "Ce paramètre permet de tester la reformulation du texte à l'aide de l'IA avant la synthèse vocale.": "Esta configuração permite testar a reformulação do texto com a ajuda da IA antes da síntese de voz.", "Clé API": "Chave API", "Clé API (Google AI Studio)": "Chave API (Google AI Studio)", "Clé API (Voice RSS)": "Chave API (RSS de voz)", @@ -199,6 +201,8 @@ "Cocher cette case désactivera les messages de mise à jour du plugin dans le centre de message": "Marcar esta caixa desativará as mensagens de atualização do plugin no centro de mensagens.", "Contournement du bug Google TTS de prononciation des mots contenant une apostrophe suivi de caractères accentués": "Contornar o bug do Google TTS na pronúncia de palavras que contêm um apóstrofo seguido de caracteres acentuados", "Convertir les SingleQuote en DoubleQuote": "Converter as aspas simples em aspas duplas", + "Critical": "Crítico", + "Debug": "Depuração", "Demande de génération du TTS de test envoyée (voir les logs du démon pour le résultat)": "Pedido de geração de TTS de teste enviado (ver resultados nos registos do daemon)", "Demande de mise à jour des listes :: Custom Sounds :: envoyée (voir les logs ttscast)": "Pedido de atualização de listas :: Sons personalizados :: enviado (ver registos ttscast)", "Demande de mise à jour des listes :: CustomRadios :: envoyée (voir les logs ttscast)": "Pedido de atualização da lista :: CustomRadios :: enviado (ver registos ttscast)", @@ -215,9 +219,11 @@ "Effacer Clé": "Eliminar tecla", "Entrez votre ID de projet Google pour l'authentification avec le moteur d'IA.": "Insira o seu ID de projeto do Google para autenticação com o motor de IA.", "Entrez votre clé API pour l'authentification avec le moteur d'IA.": "Insira a sua chave API para autenticação com o motor de IA.", + "Error": "Erro", "Espagnol (es-ES)": "Espanhol (es-ES)", "Ex: Ceci est un message de test pour la synthèse vocale à partir de Jeedom.": "Ex: Esta é uma mensagem de teste para a síntese de voz do Jeedom.", "Ex: Nest Salon": "Ex: Nest Salon", + "Ex: Parle d'une voix chaleureuse et rassurante.": "Ex.: Fala com uma voz calorosa e tranquilizadora.", "Ex: Répond à la question s'il y en a une et reformule la phrase": "Ex: Responde à pergunta, se houver alguma, e reformula a frase", "Ex: enthousiaste et humoristique": "Ex.: entusiasta e bem-humorado", "Facteur multiplicateur des cycles du démon (Défaut = x1)": "Fator de multiplicação do ciclo de demonstração (Predefinição = x1)", @@ -229,12 +235,19 @@ "Format d'encodage audio pour le moteur Google Cloud TTS (MP3 ou LINEAR16). LINEAR16 offre une meilleure qualité (WAV) mais les fichiers sont plus volumineux.": "Formato de codificação de áudio para o motor Google Cloud TTS (MP3 ou LINEAR16). O LINEAR16 oferece melhor qualidade (WAV), mas os ficheiros são maiores.", "Français (fr-FR)": "Francês (fr-FR)", "Fréquence des cycles": "Frequência do ciclo", + "Gemini 2.5": "Gemini 2.5", + "Gemini 3.x (Recommandé)": "Gemini 3.x (Recomendado)", + "Gemini TTS - Voix multilingues": "Gemini TTS - Vozes multilingues", "Google Cloud Text-To-Speech (Clé & Internet)": "Google Cloud Text-To-Speech (Tecla e Internet)", "Google Translate API (Internet)": "API do Google Translate (Internet)", "Génère les fichiers TTS à chaque demande. Il est vivement recommandé de ne PAS cocher cette case, sauf pour faire des tests": "Gera ficheiros TTS em cada pedido. Recomenda-se vivamente que NÃO assinale esta caixa, exceto para fins de teste", "Générer + Diffuser": "Gerar + Difundir", - "IA - Gemini": "IA - Gemini", + "IA & TTS - Gemini": "IA e TTS - Gemini", "Id Projet Google": "Projeto Google ID", + "Identique au démon (défaut)": "Idêntico ao daemon (padrão)", + "Info": "Informações", + "Instruction de style transmise au modèle Gemini TTS (ex: Parle d'une voix chaleureure et rassurante). Laissez vide pour le style par défaut.": "Instrução de estilo transmitida ao modelo Gemini TTS (ex.: Fale com uma voz calorosa e tranquilizadora). Deixe em branco para o estilo padrão.", + "Instruction de style transmise au modèle Gemini TTS pour toutes les notifications (ex: Parle d'une voix chaleureuse et rassurante). Peut être surchargé par l'option style: dans les scénarios. Laissez vide pour le style neutre par défaut.": "Instrução de estilo transmitida ao modelo Gemini TTS para todas as notificações (ex.: Fala com uma voz calorosa e tranquilizadora). Pode ser substituída pela opção style: nos cenários. Deixe em branco para o estilo neutro por predefinição.", "Italien (it-IT)": "Italiano (it-IT)", "Jeedom TTS (Local)": "Jeedom TTS (Local)", "LINEAR16 (WAV - 24kHz)": "LINEAR16 (WAV - 24 kHz)", @@ -276,7 +289,9 @@ "Mise à jour des listes :: Custom Sounds": "Actualizações da lista :: Sons personalizados", "Mise à jour des listes :: Radios": "Actualizações da lista :: Rádios", "Mise à jour des listes :: Sounds": "Actualizações da lista :: Sons", - "Modèle IA": "Modelo IA", + "Modèle Gemini TTS": "Modelo Gemini TTS", + "Modèle Gemini TTS à utiliser. Flash est plus économique, Pro est plus expressif.": "Modelo Gemini TTS a utilizar. O Flash é mais económico, o Pro é mais expressivo.", + "Modèle IA (Reformulation)": "Modelo IA (Reformulação)", "Modèles Stables (Recommandés)": "Modelos estáveis (recomendados)", "Moteur TTS": "Motor TTS", "Moteur TTS à utiliser pour la synthèse vocale": "Motor TTS para síntese de voz", @@ -285,6 +300,8 @@ "MàJ Radios": "Actualizações de rádio", "MàJ Sounds": "Atualização de sons", "Ne PAS utiliser le cache": "NÃO utilizar a cache", + "Niveau de log Chromecast": "Registo do Chromecast", + "Niveau de log des librairies Chromecast du démon. Par défaut, hérite du niveau de log global.": "Nível de registo das bibliotecas Chromecast do daemon. Por predefinição, herda o nível de registo global.", "Nombre de jours": "Número de dias", "Normal (0)": "Normal (0)", "Normal (1.0)": "Normal (1.0)", @@ -331,27 +348,37 @@ "SITE": "SITE", "Sauvegardez bien votre configuration AVANT d'utiliser le bouton [Générer + Diffuser]": "Guarde a sua configuração ANTES de utilizar o botão [Gerar + Transmitir]", "Serbe (sr-RS)": "Sérvio (sr-RS)", + "Si activé, la lecture audio démarre dès les premières syllabes générées, sans attendre la fin de la synthèse vocale. Réduit la latence perçue.": "Se ativada, a reprodução de áudio inicia-se logo nas primeiras sílabas geradas, sem esperar pelo fim da síntese de voz. Reduz a latência percebida.", + "Si activé, toutes les notifications utilisent Gemini TTS par défaut.": "Se ativada, todas as notificações utilizam o Gemini TTS por predefinição.", + "Si coché, le test utilise le mode streaming Gemini TTS (lecture directe sans fichier intermédiaire). Nécessite que Gemini TTS soit activé et que le paramètre 'Tester avec Gemini TTS' soit coché.": "Se esta opção estiver marcada, o teste utiliza o modo de streaming do Gemini TTS (leitura direta sem ficheiro intermédio). Requer que o Gemini TTS esteja ativado e que a opção «Testar com Gemini TTS» esteja marcada.", + "Si coché, le test utilise le moteur Gemini TTS au lieu du moteur par défaut. Nécessite que Gemini TTS soit activé.": "Se esta opção estiver marcada, o teste utiliza o motor Gemini TTS em vez do motor predefinido. É necessário que o Gemini TTS esteja ativado.", + "Streaming Gemini TTS par défaut": "Streaming Gemini TTS por predefinição", + "Style de voix (Gemini TTS)": "Estilo de voz (Gemini TTS)", + "Style par défaut (Gemini TTS)": "Estilo padrão (Gemini TTS)", "Sélectionnez le mode d'authentification à utiliser pour se connecter au moteur d'IA.": "Selecione o modo de autenticação a ser usado para se conectar ao motor de IA.", "Sélectionnez le modèle d'IA à utiliser pour la reformulation des réponses.": "Selecione o modelo de IA a ser usado para reformular as respostas.", "TTS (Text To Speech)": "TTS (Texto para fala)", "Temps (entre 0 et 3600 sec) au bout duquel la commande est dans tous les cas exécutée, même si l'équipement Google est encore en cours de lecture (par défaut : 60sec)": "Tempo (entre 0 e 3600 seg.) após o qual o comando é executado em todos os casos, mesmo que o dispositivo Google ainda esteja a ser lido (predefinição: 60 seg.)", - "Tester avec l'IA": "Testar com IA", + "Tester avec Gemini TTS": "Testar com o Gemini TTS", + "Tester avec la reformulation IA": "Testar com a reformulação da IA", "Tester avec la syntaxe SSML (TTS)": "Testes com a sintaxe SSML (TTS)", + "Tester avec le mode streaming": "Testar com o modo de streaming", "Tester la Synthèse Vocale (TTS)": "Síntese de voz de teste (TTS)", "Tests": "Testes", "Timeout": "Tempo limite", "Timeout (entre 5 et 300 sec) d'attente de la génération du fichier TTS lors d'un appel via API (par défaut : 30sec)": "Tempo limite (entre 5 e 300 seg.) para aguardar a geração do ficheiro TTS durante uma chamada através do PLC (predefinição: 30 seg.)", "Timeout de l'API 'GenerateTTS' (secondes)": "Tempo limite da API GenerateTTS (segundos)", - "Ton \/ Style": "Tom \/ Estilo", + "Ton de Reformulation": "Tom de reformulação", "URL Jeedom Externe": "URL externo do Jeedom", "Upload Clé API (OK) :: ": "Carregar chave API (OK) ::", "Upload Clé API :: Sauvegarde OK": "Carregar chave API :: Guardar OK", "Upload Custom Radios (OK) :: ": "Carregar rádios personalizados (OK) ::", "Upload Custom Sound (OK) :: ": "Carregar som personalizado (OK) ::", "Upload d'un fichier (.json) pour mettre à jour la liste des Custom Radios": "Carregar um ficheiro (.json) para atualizar a lista de rádios personalizados", - "Upload un fichier (.mp3) pour l'ajouter au répertoire des Custom Sounds": "Carregue um ficheiro (.mp3) para o adicionar à pasta Sons personalizados", + "Upload un fichier (.mp3, .wav, .ogg, .opus, .flac) pour l'ajouter au répertoire des Custom Sounds": "Carregue um ficheiro (.mp3, .wav, .ogg, .opus, .flac) para o adicionar ao diretório de sons personalizados", "Uploader votre clé JSON en utilisant le bouton 'Ajouter Clé (JSON)'": "Carregue a sua chave JSON utilizando o botão \"Adicionar chave (JSON)", "Utilise l'URL externe de Jeedom pour la lecture des fichiers TTS plutôt que l'URL interne (Recommandé = décoché)": "Utilizar o URL externo do Jeedom para ler ficheiros TTS em vez do URL interno (Recomendado = desmarcado)", + "Utiliser Gemini TTS par défaut": "Utilizar o Gemini TTS por predefinição", "Utiliser la Reformulation IA par défaut": "Usar a reformulação IA por predefinição", "Valeur par défaut = Normal (0)": "Valor por defeito = Normal (0)", "Valeur par défaut = Normal (1.0)": "Valor por defeito = Normal (1,0)", @@ -367,6 +394,9 @@ "Vitesse de Dictée (Voice RSS TTS)": "Velocidade de ditado (Voice RSS TTS)", "Vitesse de Dictée (gCloud TTS)": "Velocidade do ditado (gCloud TTS)", "Voice RSS API (Clé & Internet)": "API RSS de voz (chave e Internet)", + "Voix Gemini TTS": "Voz Gemini TTS", + "Voix utilisée par défaut. Toutes les voix Gemini TTS sont multilingues (détection automatique de la langue).": "Voz utilizada por predefinição. Todas as vozes Gemini TTS são multilingues (detecção automática do idioma).", + "Warning": "Aviso", "[ATTENTION] Ne changez ce paramètre qu'en cas de nécessité. (Défaut = 55111)": "[AVISO] Alterar este parâmetro apenas se necessário. (Predefinição = 55111)" }, "plugins\/ttscast\/plugin_info\/install.php": { diff --git a/core/php/ttscast.audio.proxy.php b/core/php/ttscast.audio.proxy.php new file mode 100644 index 00000000..78422d03 --- /dev/null +++ b/core/php/ttscast.audio.proxy.php @@ -0,0 +1,135 @@ +. + */ + +require_once dirname(__FILE__) . '/../../../../core/php/core.inc.php'; + +try { + // Protection par validation stricte des noms de fichiers (regex + identifiants imprévisibles). + $mimeTypes = [ + 'mp3' => 'audio/mp3', + 'wav' => 'audio/wav', + 'ogg' => 'audio/ogg', + 'opus' => 'audio/ogg; codecs=opus', + 'flac' => 'audio/flac', + ]; + + $type = isset($_GET['type']) ? $_GET['type'] : ''; + $file = isset($_GET['file']) ? $_GET['file'] : ''; + + if ($type === 'tts') { + // Validation stricte : MD5 hex (32 car.) + extension audio autorisée + if (!preg_match('/^([a-f0-9]{32})\.(mp3|wav|ogg|opus|flac)$/', $file, $matches)) { + log::add('ttscast', 'warning', '[PROXY][TTS] Paramètre invalide :: file=' . $file); + http_response_code(400); + die(); + } + $mime = $mimeTypes[$matches[2]]; + $filePath = dirname(dirname(__DIR__)) . '/data/cache/' . $file; + + } elseif ($type === 'stream') { + // Named pipe (mkfifo) — streaming PCM L16 Gemini TTS + // Validation : UUID v4 + extension l16 uniquement + $safeFile = basename($file); + if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.l16$/', $safeFile)) { + log::add('ttscast', 'warning', '[PROXY][Stream] Paramètre invalide :: file=' . $file); + http_response_code(400); + die(); + } + $pipePath = '/tmp/jeedom/ttscast_cache/stream/' . $safeFile; + if (!file_exists($pipePath)) { + log::add('ttscast', 'warning', '[PROXY][Stream] Pipe introuvable :: ' . $safeFile); + http_response_code(404); + die(); + } + $streamRate = in_array((int)($_GET['rate'] ?? 0), [8000, 16000, 22050, 24000, 44100, 48000], true) ? (int)$_GET['rate'] : 24000; + $streamChannels = in_array((int)($_GET['channels'] ?? 0), [1, 2], true) ? (int)$_GET['channels'] : 1; + + // Envoyer les headers HTTP + flusher AVANT d'ouvrir le pipe + // Le Chromecast reçoit « HTTP/1.1 200 OK » immédiatement et attend les données, + // au lieu de timeout pendant que PHP bloque sur fopen() du FIFO. + header('Content-Type: audio/wav'); // WAV RIFF avec header dans le stream (PCM LE 16-bit) + header('Cache-Control: no-cache, no-store'); + header('Content-Encoding: identity'); // Désactiver mod_deflate — le PCM brut ne doit pas être compressé + set_time_limit(0); + while (ob_get_level() > 0) { + ob_end_clean(); + } + ob_implicit_flush(true); // Flush automatique à chaque echo + flush(); // Envoyer les headers HTTP maintenant (avant que le pipe soit ouvert) + + log::add('ttscast', 'debug', '[PROXY][Stream] Ouverture du pipe (attente writer) :: ' . $safeFile); + $fh = fopen($pipePath, 'rb'); + if ($fh === false) { + log::add('ttscast', 'error', '[PROXY][Stream] Échec ouverture pipe :: ' . $safeFile); + die(); + } + log::add('ttscast', 'debug', '[PROXY][Stream] Pipe ouvert, streaming démarré :: rate=' . $streamRate . ' | channels=' . $streamChannels); + + // Lire le pipe et envoyer immédiatement au client (Chromecast) + $bytesSent = 0; + while (!feof($fh)) { + $chunk = fread($fh, 8192); + if ($chunk !== false && $chunk !== '') { + echo $chunk; + $bytesSent += strlen($chunk); + } + } + fclose($fh); + log::add('ttscast', 'debug', '[PROXY][Stream] Terminé :: ' . $safeFile . ' | ' . $bytesSent . ' bytes envoyés'); + // Nettoyage du pipe (déjà supprimé par le démon Python mais au cas où) + if (file_exists($pipePath)) { + @unlink($pipePath); + } + die(); + + } elseif ($type === 'sounds' || $type === 'customsounds') { + // Validation : nom de fichier sûr (pas de séparateur de répertoire, extension autorisée) + $safeFile = basename($file); + if (!preg_match('/^([a-zA-Z0-9._-]+)\.(mp3|wav|ogg|opus|flac)$/', $safeFile, $matches)) { + log::add('ttscast', 'warning', '[PROXY][Sounds] Paramètre invalide :: file=' . $file); + http_response_code(400); + die(); + } + $mime = $mimeTypes[$matches[2]]; + $subDir = ($type === 'customsounds') ? 'custom/' : ''; + $filePath = dirname(dirname(__DIR__)) . '/data/media/' . $subDir . $safeFile; + + } else { + log::add('ttscast', 'warning', '[PROXY] Type inconnu :: type=' . $type); + http_response_code(400); + die(); + } + + if (!file_exists($filePath) || !is_file($filePath)) { + log::add('ttscast', 'warning', '[PROXY] Fichier non trouvé :: ' . basename($filePath)); + http_response_code(404); + die(); + } + + $size = filesize($filePath); + header('Content-Type: ' . $mime); + header('Content-Length: ' . $size); + header('Accept-Ranges: bytes'); + header('Cache-Control: no-cache, no-store'); + readfile($filePath); + die(); + +} catch (Exception $e) { + log::add('ttscast', 'error', '[PROXY] Exception :: ' . $e->getMessage()); + http_response_code(500); + die(); +} diff --git a/data/.htaccess b/data/.htaccess index 8292c2d2..042082fb 100644 --- a/data/.htaccess +++ b/data/.htaccess @@ -1,5 +1,5 @@ Options +FollowSymLinks - + Require all granted Require all denied \ No newline at end of file diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..4763a9eb --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,2 @@ +parameters: + reportUnmatchedIgnoredErrors: true diff --git a/plugin_info/configuration.php b/plugin_info/configuration.php index cf217350..24509d94 100644 --- a/plugin_info/configuration.php +++ b/plugin_info/configuration.php @@ -113,6 +113,22 @@ +
+ +
+ +
+
{{TTS (Text To Speech)}}
@@ -264,6 +280,7 @@
@@ -730,7 +747,7 @@
- {{IA - Gemini}} + {{IA & TTS - Gemini}}
+
-
-
- -
- -
-
-
-
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
{{Tests}}
-
+
+ +
+ +
+
+ +
@@ -1000,6 +1139,8 @@ AI_AUTH_MODE: '.customform-ai-authmode', AI_APIKEY: '.customform-ai-apikey', AI_OAUTH2: '.customform-ai-oauth2', + GEMINI_TTS: '.customform-geminiTTS', + TEST_GEMINI_STYLE: '.customform-testGeminiStyle', ADDRESS_CHECKBOX: '.customform-address', ADDRESS_TEST_URL: '.addressTestURL', API_KEY_INPUT: '.custominput-apikey', @@ -1138,6 +1279,32 @@ function initConfigurationPage() { if (aiAuthModeEl) { aiAuthModeEl.addEventListener('change', aiAuthModeSelect) } + + // Listen for Gemini TTS enable toggle + const geminiTTSToggle = document.querySelector('[data-l1key="geminiTTSEnabled"]') + if (geminiTTSToggle) { + const geminiTTSSections = document.querySelectorAll(SELECTORS.GEMINI_TTS) + const updateGeminiTTSVisibility = () => { + geminiTTSSections.forEach(el => { + el.style.display = geminiTTSToggle.checked ? '' : 'none' + }) + } + geminiTTSToggle.addEventListener('change', updateGeminiTTSVisibility) + updateGeminiTTSVisibility() + } + + // Listen for Gemini TTS test toggle (show/hide style field) + const ttsTestGeminiToggle = document.querySelector('[data-l1key="ttsTestGemini"]') + if (ttsTestGeminiToggle) { + const testGeminiStyleSections = document.querySelectorAll(SELECTORS.TEST_GEMINI_STYLE) + const updateTestGeminiStyleVisibility = () => { + testGeminiStyleSections.forEach(el => { + el.style.display = ttsTestGeminiToggle.checked ? '' : 'none' + }) + } + ttsTestGeminiToggle.addEventListener('change', updateTestGeminiStyleVisibility) + updateTestGeminiStyleVisibility() + } } // Initialize when DOM is ready diff --git a/plugin_info/info.json b/plugin_info/info.json index 30d67fdc..13f0fb2e 100644 --- a/plugin_info/info.json +++ b/plugin_info/info.json @@ -1,7 +1,7 @@ { "id": "ttscast", "name": "TTS Cast", - "pluginVersion": "1.8.14", + "pluginVersion": "1.9.5", "description": { "fr_FR": "Plugin pour gérer ses équipements Google, type Google Home, Nest Mini, Nest Hub (Max), Chromecast. Il permet de générer des notifications TTS (Text To Speech) et de les diffuser sur les équipements Google. Il permet également de diffuser des sons (mp3), des vidéos YouTube, une page Web, ou encore une radio en streaming sur ces mêmes équipements.", "en_US": "Plugin to manage Google equipment, such as Google Home, Nest Mini, Nest Hub (Max), Chromecast. It allows you to generate TTS (Text To Speech) notifications and broadcast them to Google devices. It also allows you to broadcast sounds (mp3), YouTube videos, a web page, or even streaming radio on the same equipment.", diff --git a/plugin_info/install.php b/plugin_info/install.php index 704ab34e..7ee91c55 100644 --- a/plugin_info/install.php +++ b/plugin_info/install.php @@ -85,6 +85,12 @@ function ttscast_install() { if (config::byKey('ttsAIDefault', 'ttscast') == '') { config::save('ttsAIDefault', '0', 'ttscast'); } + if (config::byKey('streamingDefault', 'ttscast') == '') { + config::save('streamingDefault', '0', 'ttscast'); + } + if (config::byKey('castLogLevel', 'ttscast') == '') { + config::save('castLogLevel', 'daemon', 'ttscast'); + } if (config::byKey('disableUpdateMsg', 'ttscast') == '') { config::save('disableUpdateMsg', '0', 'ttscast'); } @@ -171,6 +177,12 @@ function ttscast_update() { if (config::byKey('ttsAIDefault', 'ttscast') == '') { config::save('ttsAIDefault', '0', 'ttscast'); } + if (config::byKey('streamingDefault', 'ttscast') == '') { + config::save('streamingDefault', '0', 'ttscast'); + } + if (config::byKey('castLogLevel', 'ttscast') == '') { + config::save('castLogLevel', 'daemon', 'ttscast'); + } if (config::byKey('disableUpdateMsg', 'ttscast') == '') { config::save('disableUpdateMsg', '0', 'ttscast'); } diff --git a/resources/requirements.txt b/resources/requirements.txt index 99844eab..9de0dd00 100644 --- a/resources/requirements.txt +++ b/resources/requirements.txt @@ -1,6 +1,6 @@ PyChromecast==14.0.10 google-cloud-texttospeech==2.36.0 gTTS==2.5.4 -google-genai==2.2.0 +google-genai==2.4.0 Markdown==3.10.2 beautifulsoup4==4.14.3 \ No newline at end of file diff --git a/resources/ttscastd/ttscastd.py b/resources/ttscastd/ttscastd.py index 613e6657..91ec258b 100644 --- a/resources/ttscastd/ttscastd.py +++ b/resources/ttscastd/ttscastd.py @@ -28,10 +28,12 @@ import queue import requests import re +import errno import wave +import io -from urllib.parse import urlencode, urlparse -from uuid import UUID +from urllib.parse import urlencode, urlparse, quote +from uuid import UUID, uuid4 # Import pour Jeedom try: @@ -142,46 +144,46 @@ def eventsFromJeedom(cycle=0.5): # Traitement des actions (inclus les CustomCmd) if message['cmd_action'] == 'ttstest': - logging.debug('[DAEMON][SOCKET] Generate And Play Test TTS') + logging.info('[DAEMON][SOCKET] Generate And Play Test TTS') - if all(keys in message for keys in ('ttsText', 'ttsGoogleName', 'ttsVoiceName', 'ttsLang', 'ttsEngine', 'ttsSpeed', 'ttsRSSSpeed', 'ttsRSSVoiceName', 'ttsSSML', 'ttsAI')): - logging.debug('[DAEMON][SOCKET] Test TTS :: %s', message['ttsText'] + ' | ' + message['ttsGoogleName'] + ' | ' + message['ttsVoiceName'] + ' | ' + message['ttsLang'] + ' | ' + message['ttsEngine'] + ' | ' + message['ttsSpeed'] + ' | ' + message['ttsRSSVoiceName'] + ' | ' + message['ttsRSSSpeed'] + ' | ' + message['ttsSSML'] + ' | ' + message['ttsAI']) - threading.Thread(target=TTSCast.generateTestTTS, args=[message['ttsText'], message['ttsGoogleName'], message['ttsVoiceName'], message['ttsRSSVoiceName'], message['ttsLang'], message['ttsEngine'], message['ttsSpeed'], message['ttsRSSSpeed'], message['ttsSSML'], message['ttsAI']]).start() + if all(keys in message for keys in ('ttsText', 'ttsGoogleName', 'ttsVoiceName', 'ttsLang', 'ttsEngine', 'ttsSpeed', 'ttsRSSSpeed', 'ttsRSSVoiceName', 'ttsGeminiVoiceName', 'ttsGeminiStyle', 'ttsSSML', 'ttsAI', 'ttsGemini', 'ttsStreaming')): + logging.debug('[DAEMON][SOCKET] Test TTS :: %s', message['ttsText'] + ' | ' + message['ttsGoogleName'] + ' | ' + message['ttsVoiceName'] + ' | ' + message['ttsLang'] + ' | ' + message['ttsEngine'] + ' | ' + message['ttsSpeed'] + ' | ' + message['ttsRSSVoiceName'] + ' | ' + message['ttsRSSSpeed'] + ' | ' + message['ttsSSML'] + ' | ' + message['ttsAI'] + ' | geminiVoice=' + message['ttsGeminiVoiceName'] + ' | geminiStyle=' + message['ttsGeminiStyle'] + ' | gemini=' + message['ttsGemini'] + ' | streaming=' + message['ttsStreaming']) + threading.Thread(target=TTSCast.generateTestTTS, args=[message['ttsText'], message['ttsGoogleName'], message['ttsVoiceName'], message['ttsRSSVoiceName'], message['ttsGeminiVoiceName'], message['ttsGeminiStyle'], message['ttsLang'], message['ttsEngine'], message['ttsSpeed'], message['ttsRSSSpeed'], message['ttsSSML'], message['ttsAI'], message['ttsGemini'], message['ttsStreaming']]).start() else: - logging.debug('[DAEMON][SOCKET] Test TTS :: Il manque des données pour traiter la commande.') + logging.warning('[DAEMON][SOCKET] Test TTS :: Il manque des données pour traiter la commande.') elif message['cmd_action'] == 'tts': - logging.debug('[DAEMON][SOCKET] Generate And Play TTS') + logging.info('[DAEMON][SOCKET] Generate And Play TTS') - if all(keys in message for keys in ('ttsText', 'ttsGoogleUUID', 'ttsVoiceName', 'ttsLang', 'ttsEngine', 'ttsSpeed', 'ttsOptions', 'ttsRSSSpeed', 'ttsRSSVoiceName')): + if all(keys in message for keys in ('ttsText', 'ttsGoogleUUID', 'ttsVoiceName', 'ttsLang', 'ttsEngine', 'ttsSpeed', 'ttsOptions', 'ttsRSSSpeed', 'ttsRSSVoiceName', 'ttsGeminiVoiceName')): logging.debug('[DAEMON][SOCKET] TTS :: %s', str(message)) - threading.Thread(target=TTSCast.getTTS, args=[message['ttsText'], message['ttsGoogleUUID'], message['ttsVoiceName'], message['ttsRSSVoiceName'], message['ttsLang'], message['ttsEngine'], message['ttsSpeed'], message['ttsRSSSpeed'], message['ttsOptions'], message.get('cmdNotificationId', 0)]).start() + threading.Thread(target=TTSCast.getTTS, args=[message['ttsText'], message['ttsGoogleUUID'], message['ttsVoiceName'], message['ttsRSSVoiceName'], message['ttsGeminiVoiceName'], message['ttsLang'], message['ttsEngine'], message['ttsSpeed'], message['ttsRSSSpeed'], message['ttsOptions'], message.get('cmdNotificationId', 0)]).start() else: - logging.debug('[DAEMON][SOCKET] TTS :: Il manque des données pour traiter la commande.') + logging.warning('[DAEMON][SOCKET] TTS :: Il manque des données pour traiter la commande.') elif message['cmd_action'] == 'generatetts': - logging.debug('[DAEMON][SOCKET] Generate TTS as Jeedom Engine') + logging.info('[DAEMON][SOCKET] Generate TTS as Jeedom Engine') - if all(keys in message for keys in ('ttsText', 'ttsFile', 'ttsVoiceName', 'ttsLang', 'ttsEngine', 'ttsSpeed', 'ttsOptions', 'ttsRSSSpeed', 'ttsRSSVoiceName')): + if all(keys in message for keys in ('ttsText', 'ttsFile', 'ttsVoiceName', 'ttsLang', 'ttsEngine', 'ttsSpeed', 'ttsOptions', 'ttsRSSSpeed', 'ttsRSSVoiceName', 'ttsGeminiVoiceName')): logging.debug('[DAEMON][SOCKET] GenerateTTS :: %s', str(message)) - threading.Thread(target=TTSCast.generateTTS, args=[message['ttsText'], message['ttsFile'], message['ttsVoiceName'], message['ttsRSSVoiceName'], message['ttsLang'], message['ttsEngine'], message['ttsSpeed'], message['ttsRSSSpeed'], message['ttsOptions']]).start() + threading.Thread(target=TTSCast.generateTTS, args=[message['ttsText'], message['ttsFile'], message['ttsVoiceName'], message['ttsRSSVoiceName'], message['ttsGeminiVoiceName'], message['ttsLang'], message['ttsEngine'], message['ttsSpeed'], message['ttsRSSSpeed'], message['ttsOptions']]).start() else: - logging.debug('[DAEMON][SOCKET] GenerateTTS :: Il manque des données pour traiter la commande.') + logging.warning('[DAEMON][SOCKET] GenerateTTS :: Il manque des données pour traiter la commande.') elif (message['cmd_action'] == 'volumeset' and all(keys in message for keys in ('value', 'googleUUID'))): - logging.debug('[DAEMON][SOCKET] Action :: VolumeSet = %s @ %s', message['value'], message['googleUUID']) + logging.info('[DAEMON][SOCKET] Action :: VolumeSet = %s @ %s', message['value'], message['googleUUID']) threading.Thread(target=Functions.mediaActions, args=[message['googleUUID'], message['value'], message['cmd_action']]).start() elif (message['cmd_action'] in ('volumeup', 'volumedown', 'media_pause', 'media_play', 'media_stop', 'media_next', 'media_quit', 'media_rewind', 'media_previous', 'mute_on', 'mute_off') and 'googleUUID' in message): - logging.debug('[DAEMON][SOCKET] Action :: %s @ %s', message['cmd_action'], message['googleUUID']) + logging.info('[DAEMON][SOCKET] Action :: %s @ %s', message['cmd_action'], message['googleUUID']) threading.Thread(target=Functions.mediaActions, args=[message['googleUUID'], '', message['cmd_action']]).start() elif (message['cmd_action'] in ('youtube', 'dashcast', 'radios', 'customradios', 'sounds', 'customsounds', 'media', 'start_app')): - logging.debug('[DAEMON][SOCKET] Media :: %s @ %s', message['cmd_action'], message['googleUUID']) + logging.info('[DAEMON][SOCKET] Media :: %s @ %s', message['cmd_action'], message['googleUUID']) threading.Thread(target=Functions.controllerActions, args=[message['googleUUID'], message['cmd_action'], message['value'], message['options']]).start() elif message['cmd'] == 'purgettscache': - logging.debug('[DAEMON][SOCKET] Purge TTS Cache') + logging.info('[DAEMON][SOCKET] Purge TTS Cache') if 'days' in message: threading.Thread(target=Functions.purgeCache, args=[message['days']]).start() @@ -300,7 +302,7 @@ def mainLoop(cycle=2): logging.debug(traceback.format_exc()) shutdown() except KeyboardInterrupt: - logging.error('[DAEMON][MAINLOOP] KeyboardInterrupt on MainLoop, Shutdown.') + logging.info('[DAEMON][MAINLOOP] KeyboardInterrupt on MainLoop, Shutdown.') shutdown() class TTSCast: @@ -332,7 +334,7 @@ def jeedomTTS(ttsText, ttsLang): if response.status_code != requests.codes.ok: filecontent = None - logging.error('[DAEMON][JeedomTTS] Status Code Error :: %s', response.status_code) + logging.warning('[DAEMON][JeedomTTS] Status Code Error :: %s (%s)', response.status_code, response.reason) else: if len(response.content) < 254 and os.path.exists(response.content): logging.debug('[DAEMON][JeedomTTS] Response is a FilePath. Downloading Content Now.') @@ -340,7 +342,7 @@ def jeedomTTS(ttsText, ttsLang): filecontent = fc.read() fc.close() except Exception as e: - logging.error('[DAEMON][JeedomTTS] Error while retrieving TTS file :: %s', e) + logging.error('[DAEMON][JeedomTTS] Error while retrieving TTS file :: %s | lang : %s | extrait : %s', e, ttsLang, repr(ttsText[:80])) logging.debug(traceback.format_exc()) filecontent = None return filecontent @@ -375,14 +377,14 @@ def voiceRSS(ttsText, ttsLang, ttsSpeed='0', ttsSSML=False): if response.status_code != requests.codes.ok: filecontent = None - logging.error('[DAEMON][VoiceRSS] Status Code Error :: %s (%s)', response.status_code, response.reason) + logging.warning('[DAEMON][VoiceRSS] Status Code Error :: %s (%s)', response.status_code, response.reason) else: """ logging.debug('[DAEMON][VoiceRSS] Response is OK. Downloading Content Now.') fc = open(response.content, "rb") filecontent = fc.read() fc.close() """ except Exception as e: - logging.error('[DAEMON][VoiceRSS] Error while retrieving TTS file :: %s', e) + logging.error('[DAEMON][VoiceRSS] Error while retrieving TTS file :: %s | voix : %s | extrait : %s', e, ttsVoiceName, repr(ttsText[:80])) logging.debug(traceback.format_exc()) filecontent = None return filecontent @@ -407,7 +409,11 @@ def _sendTTSResult(text, isTest, googleUUID='', cmdNotificationId=0, cmdOpts=Non logging.debug('[DAEMON][TTS] ttsNotifyResult envoyé :: cmdNotificationId=%s', cmdNotificationId) @staticmethod - def generateTestTTS(ttsText, ttsGoogleName, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEngine, ttsSpeed='1.0', ttsRSSSpeed='0', ttsSSML='0', ttsAI='0'): + def generateTestTTS(ttsText, ttsGoogleName, ttsVoiceName, ttsRSSVoiceName, ttsGeminiVoiceName, ttsGeminiStyle, ttsLang, ttsEngine, ttsSpeed='1.0', ttsRSSSpeed='0', ttsSSML='0', ttsAI='0', ttsGemini='0', ttsStreaming='0'): + # Override moteur si test Gemini TTS activé + if ttsGemini == '1' and myConfig.geminiTTSEnabled: + ttsEngine = 'geminitts' + logging.debug('[DAEMON][TestTTS] Param TTSEngine :: %s', ttsEngine) logging.debug('[DAEMON][TestTTS] Check des répertoires') @@ -434,9 +440,7 @@ def generateTestTTS(ttsText, ttsGoogleName, ttsVoiceName, ttsRSSVoiceName, ttsLa credentials = service_account.Credentials.from_service_account_file(os.path.join(myConfig.configFullPath, myConfig.gCloudApiKey)) logging.debug('[DAEMON][TestTTS] Test et génération du fichier TTS (mp3/wav)') - # _ext = ".wav" if myConfig.gCloudAudioEncoding == "LINEAR16" else ".mp3" - # Force extension MP3 to ensure playback on all devices even if content is WAV - _ext = ".mp3" + _ext = ".wav" if myConfig.gCloudAudioEncoding == "LINEAR16" else ".mp3" raw_filename = ttsText + "|gCloudTTS|" + ttsVoiceName + "|" + ttsSpeed + "|" + ttsSSML + "|" + myConfig.gCloudAudioEncoding filename = hashlib.md5(raw_filename.encode('utf-8')).hexdigest() + _ext filepath = os.path.join(symLinkPath, filename) @@ -459,7 +463,7 @@ def generateTestTTS(ttsText, ttsGoogleName, ttsVoiceName, ttsRSSVoiceName, ttsLa TTSCast._sendTTSResult(_aiReformulatedText, True) text_input = googleCloudTTS.SynthesisInput(text=ttsAIText) else: - logging.error('[DAEMON][TestTTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') + logging.warning('[DAEMON][TestTTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') if myConfig.appConvertSingleQuote: ttsText = Functions.convertSingleQuoteToDoubleQuote(ttsText, True, "TestTTS") text_input = googleCloudTTS.SynthesisInput(text=ttsText) @@ -509,9 +513,7 @@ def generateTestTTS(ttsText, ttsGoogleName, ttsVoiceName, ttsRSSVoiceName, ttsLa urlFileToPlay = f'{ttsSrvWeb}{filename}' logging.debug('[DAEMON][TestTTS] URL du fichier TTS à diffuser :: %s', urlFileToPlay) - # _mimeType = "audio/wav" if myConfig.gCloudAudioEncoding == "LINEAR16" else "audio/mp3" - # Force mimetype MP3 to ensure playback on all devices even if content is WAV - _mimeType = "audio/mp3" + _mimeType = "audio/wav" if myConfig.gCloudAudioEncoding == "LINEAR16" else "audio/mp3" res = TTSCast.castToGoogleHome(urlFileToPlay, ttsGoogleName, mimeType=_mimeType) logging.debug('[DAEMON][TestTTS] Résultat de la lecture du TTS sur le Google Home :: %s', str(res)) @@ -536,7 +538,7 @@ def generateTestTTS(ttsText, ttsGoogleName, ttsVoiceName, ttsRSSVoiceName, ttsLa TTSCast._sendTTSResult(_aiReformulatedText, True) ttsText = ttsAIText else: - logging.error('[DAEMON][TestTTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') + logging.warning('[DAEMON][TestTTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') client = gTTS(ttsText, lang=langToTTS) client.save(filepath) except Exception as e: @@ -545,7 +547,7 @@ def generateTestTTS(ttsText, ttsGoogleName, ttsVoiceName, ttsRSSVoiceName, ttsLa os.remove(filepath) except OSError: pass - logging.debug('[DAEMON][TestTTS] Google Translate API ERROR :: %s', e) + logging.warning('[DAEMON][TestTTS] Google Translate API ERROR :: %s | lang : %s | extrait : %s', e, ttsLang, repr(ttsText[:80])) else: logging.debug('[DAEMON][TestTTS] Le fichier TTS existe déjà dans le cache :: %s', filepath) @@ -573,13 +575,13 @@ def generateTestTTS(ttsText, ttsGoogleName, ttsVoiceName, ttsRSSVoiceName, ttsLa TTSCast._sendTTSResult(_aiReformulatedText, True) ttsText = ttsAIText else: - logging.error('[DAEMON][TestTTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') + logging.warning('[DAEMON][TestTTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') ttsResult = TTSCast.jeedomTTS(ttsText, ttsLang) if ttsResult is not None: with open(filepath, 'wb') as f: f.write(ttsResult) else: - logging.debug('[DAEMON][TestTTS] JeedomTTS Error :: Incorrect Output') + logging.warning('[DAEMON][TestTTS] JeedomTTS Error :: Incorrect Output — lang : %s — extrait : %s', ttsLang, repr(ttsText[:80])) else: logging.debug('[DAEMON][TestTTS] Le fichier TTS existe déjà dans le cache :: %s', filepath) @@ -608,13 +610,13 @@ def generateTestTTS(ttsText, ttsGoogleName, ttsVoiceName, ttsRSSVoiceName, ttsLa TTSCast._sendTTSResult(_aiReformulatedText, True) ttsText = ttsAIText else: - logging.error('[DAEMON][TestTTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') + logging.warning('[DAEMON][TestTTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') ttsResult = TTSCast.voiceRSS(ttsText, ttsRSSVoiceName, ttsRSSSpeed, True if ttsSSML == '1' else False) if ttsResult is not None: with open(filepath, 'wb') as f: f.write(ttsResult) else: - logging.debug('[DAEMON][TestTTS] VoiceRSS Error :: Incorrect Output') + logging.warning('[DAEMON][TestTTS] VoiceRSS Error :: Incorrect Output — voix : %s — extrait : %s', ttsRSSVoiceName, repr(ttsText[:80])) else: logging.debug('[DAEMON][TestTTS] Le fichier TTS existe déjà dans le cache :: %s', filepath) @@ -628,18 +630,90 @@ def generateTestTTS(ttsText, ttsGoogleName, ttsVoiceName, ttsRSSVoiceName, ttsLa else: logging.warning('[DAEMON][TestTTS] Clé API (Voice RSS) invalide :: ' + myConfig.apiRSSKey) + elif ttsEngine == "geminitts": + logging.debug('[DAEMON][TestTTS] TTSEngine = geminitts') + # Pas de cache pour Gemini TTS : sortie LLM non déterministe, toujours régénérer + raw_filename = ttsText + "|GeminiTTS|" + ttsGeminiVoiceName + "|" + myConfig.geminiTTSModel + filename = hashlib.md5(raw_filename.encode('utf-8')).hexdigest() + ".wav" + filepath = os.path.join(symLinkPath, filename) + logging.debug('[DAEMON][TestTTS] Nom du fichier à générer :: %s', filepath) + + _textToSynth = ttsText + if ttsAI == '1': + ttsAIText = TTSCast.genAI(ttsText, _aiCustomSysPrompt) + if ttsAIText is not None: + logging.debug('[DAEMON][TestTTS] Génération Gemini TTS avec IA') + _aiReformulatedText = ttsAIText + TTSCast._sendTTSResult(_aiReformulatedText, True) + _textToSynth = ttsAIText + else: + logging.warning('[DAEMON][TestTTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') + if myConfig.appConvertSingleQuote: + _textToSynth = Functions.convertSingleQuoteToDoubleQuote(_textToSynth, True, "TestTTS") + _effectiveStyle = ttsGeminiStyle if ttsGeminiStyle else myConfig.geminiTTSStyle + + if _aiReformulatedText is None: + TTSCast._sendTTSResult(ttsText, True) + + if ttsStreaming == '1': + logging.debug('[DAEMON][TestTTS] Mode streaming activé pour Gemini TTS') + _t = time.time() + logging.info('[TIMING][GeminiStream] t0_start :: %.3f (%s)', _t, datetime.datetime.fromtimestamp(_t).strftime('%H:%M:%S.') + f'{int((_t % 1) * 1000):03d}') + prefetch = TTSCast.geminiTTS(_textToSynth, ttsGeminiVoiceName, _effectiveStyle, streaming=True) + if prefetch is None: + logging.error('[DAEMON][TestTTS] GeminiTTS streaming :: échec de la pré-lecture | voix : %s | extrait : %s', ttsGeminiVoiceName, repr(_textToSynth[:80])) + return + streamIter, firstChunkBytes, sampleRate, channels, streamClient = prefetch + streamMimeType = 'audio/wav' # Le proxy envoie un stream WAV RIFF (PCM LE 16-bit) + streamDir = myConfig.ttsStreamFolderTmp + os.makedirs(streamDir, exist_ok=True) + pipeName = str(uuid4()) + '.l16' + pipePath = os.path.join(streamDir, pipeName) + os.mkfifo(pipePath) # type: ignore[attr-defined] # POSIX only — cible Debian + pipeUrl = f'{myConfig.ttsWebSrvMediaProxy}?type=stream&file={pipeName}&rate={sampleRate}&channels={channels}' + logging.debug('[DAEMON][TestTTS] Pipe créé :: %s | URL :: %s', pipePath, pipeUrl) + threading.Thread(target=TTSCast.geminiTTSStream, args=[streamIter, firstChunkBytes, sampleRate, channels, pipePath, filepath, streamClient]).start() + _t = time.time() + logging.info('[TIMING][GeminiStream] t1_castStart :: %.3f (%s)', _t, datetime.datetime.fromtimestamp(_t).strftime('%H:%M:%S.') + f'{int((_t % 1) * 1000):03d}') + res = TTSCast.castToGoogleHome(pipeUrl, ttsGoogleName, mimeType=streamMimeType, streamType='LIVE') + else: + audioBytes = TTSCast.geminiTTS(_textToSynth, ttsGeminiVoiceName, _effectiveStyle) + if isinstance(audioBytes, bytes): + with open(filepath, 'wb') as out: + out.write(audioBytes) + logging.debug('[DAEMON][TestTTS] Fichier TTS Gemini généré :: %s', filepath) + else: + logging.error('[DAEMON][TestTTS] Echec de la génération Gemini TTS — voix : %s — extrait : %s', ttsGeminiVoiceName, repr(_textToSynth[:80])) + return + urlFileToPlay = f'{ttsSrvWeb}{filename}' + logging.debug('[DAEMON][TestTTS] URL du fichier TTS à diffuser :: %s', urlFileToPlay) + res = TTSCast.castToGoogleHome(urlFileToPlay, ttsGoogleName, mimeType='audio/wav') + + logging.debug('[DAEMON][TestTTS] Résultat de la lecture du TTS sur le Google Home :: %s', str(res)) + @staticmethod - def generateTTS(ttsText, ttsFile, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEngine, ttsSpeed='1.0', ttsRSSSpeed='0', ttsOptions=None): + def generateTTS(ttsText, ttsFile, ttsVoiceName, ttsRSSVoiceName, ttsGeminiVoiceName, ttsLang, ttsEngine, ttsSpeed='1.0', ttsRSSSpeed='0', ttsOptions=None): + """ + Génère un fichier audio TTS à la demande du Core Jeedom (TTSCast utilisé comme moteur TTS natif). + + Appelé via socket 'generatetts' → ttscast::generateTTS() PHP → hook tts() du Core. + Le fichier résultant est écrit à l'emplacement ttsFile et le Core attend sa création. + """ try: if not ttsOptions: ttsOptions = None - + + # Override moteur si Gemini TTS est le moteur par défaut + if myConfig.geminiTTSDefault and myConfig.geminiTTSEnabled: + ttsEngine = 'geminitts' + _useAI = myConfig.aiDefault if myConfig.aiEnabled else False _aiCustomTone = None _aiCustomSysPrompt = myConfig.aiCustomSysPrompt if (myConfig.aiEnabled and myConfig.aiUseCustomSysPrompt) else None _aiCustomTemp = None _useSSML = False _silenceBefore = None + _ttsGeminiStyle = myConfig.geminiTTSStyle try: if (ttsOptions is not None): @@ -655,13 +729,22 @@ def generateTTS(ttsText, ttsFile, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEng _useSSML = options_json.get('ssml', False) # Before _silenceBefore = options_json.get('before', None) + # Gemini TTS Style + _ttsGeminiStyle = options_json.get('style', _ttsGeminiStyle) + # Engine override (par notification) + _requestedEngine = options_json.get('engine', None) + if _requestedEngine is not None: + if _requestedEngine == 'geminitts' and not myConfig.geminiTTSEnabled: + logging.error('[DAEMON][GenerateTTS] Option "engine: geminitts" refusée : Gemini TTS n\'est pas activé dans la configuration du plugin. Activez Gemini TTS dans la configuration avant d\'utiliser cette option. Aucun TTS diffusé.') + return False + ttsEngine = _requestedEngine if _silenceBefore is not None and _useSSML is False: _useSSML = True ttsText = "

" + ttsText + "

" logging.debug('[DAEMON][GenerateTTS] Ajout de %s de silence avant le TTS :: %s', str(_silenceBefore), ttsText) elif _silenceBefore is not None and _useSSML is True: - logging.error('[DAEMON][GenerateTTS] Les options "before" et "ssml" ne peuvent pas être utilisées dans la même commande.') + logging.error('[DAEMON][GenerateTTS] Les options "before" et "ssml" ne peuvent pas être utilisées dans la même commande. Aucun TTS diffusé.') return False # Custom Voice @@ -676,10 +759,13 @@ def generateTTS(ttsText, ttsFile, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEng elif ttsEngine == "voicersstts": ttsRSSVoiceName = _ttsVoiceCode logging.debug('[DAEMON][GenerateTTS] Voix Custom (VoiceRSS) :: %s', ttsRSSVoiceName) - + elif ttsEngine == "geminitts": + ttsGeminiVoiceName = _ttsVoiceCode + logging.debug('[DAEMON][GenerateTTS] Voix Custom (Gemini TTS) :: %s', ttsGeminiVoiceName) + logging.debug('[DAEMON][GenerateTTS] Options :: %s', str(options_json)) except ValueError as e: - logging.debug('[DAEMON][GenerateTTS] Options mal formatées (Json KO) :: %s', e) + logging.warning('[DAEMON][GenerateTTS] Options mal formatées (Json KO) :: %s', e) if ttsEngine == "gcloudtts": logging.debug('[DAEMON][GenerateTTS] TTSEngine = gcloudtts') @@ -709,7 +795,7 @@ def generateTTS(ttsText, ttsFile, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEng ttsAIText = Functions.convertSingleQuoteToDoubleQuote(ttsAIText) text_input = googleCloudTTS.SynthesisInput(text=ttsAIText) else: - logging.error('[DAEMON][GenerateTTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') + logging.warning('[DAEMON][GenerateTTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') if myConfig.appConvertSingleQuote: ttsText = Functions.convertSingleQuoteToDoubleQuote(ttsText) text_input = googleCloudTTS.SynthesisInput(text=ttsText) @@ -765,7 +851,7 @@ def generateTTS(ttsText, ttsFile, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEng logging.debug('[DAEMON][GenerateTTS] Génération du TTS avec IA') ttsText = ttsAIText else: - logging.error('[DAEMON][GenerateTTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') + logging.warning('[DAEMON][GenerateTTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') client = gTTS(ttsText, lang=langToTTS) client.save(filepath) logging.debug('[DAEMON][GenerateTTS] Fichier TTS généré :: %s', filepath) @@ -775,7 +861,7 @@ def generateTTS(ttsText, ttsFile, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEng os.remove(filepath) except OSError: pass - logging.debug('[DAEMON][GenerateTTS] Google Translate API ERROR :: %s', e) + logging.warning('[DAEMON][GenerateTTS] Google Translate API ERROR :: %s | lang : %s | extrait : %s', e, ttsLang, repr(ttsText[:80])) else: logging.debug('[DAEMON][GenerateTTS] Le fichier TTS existe déjà dans le cache :: %s', filepath) @@ -793,25 +879,51 @@ def generateTTS(ttsText, ttsFile, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEng logging.debug('[DAEMON][GenerateTTS] Génération du TTS avec IA') ttsText = ttsAIText else: - logging.error('[DAEMON][GenerateTTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') + logging.warning('[DAEMON][GenerateTTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') ttsResult = TTSCast.voiceRSS(ttsText, ttsRSSVoiceName, ttsRSSSpeed, _useSSML) if ttsResult is not None: with open(filepath, 'wb') as f: f.write(ttsResult) logging.debug('[DAEMON][GenerateTTS] Fichier TTS généré :: %s', filepath) else: - logging.debug('[DAEMON][GenerateTTS] VoiceRSS Error :: Incorrect Output') + logging.warning('[DAEMON][GenerateTTS] VoiceRSS Error :: Incorrect Output — voix : %s — extrait : %s', ttsRSSVoiceName, repr(ttsText[:80])) else: logging.debug('[DAEMON][GenerateTTS] Le fichier TTS existe déjà dans le cache :: %s', filepath) else: - logging.warning('[DAEMON][GenerateTTS] Clé API (Voice RSS) invalide :: ' + myConfig.apiRSSKey) - + logging.warning('[DAEMON][GenerateTTS] Clé API (Voice RSS) invalide :: ' + myConfig.apiRSSKey) + + elif ttsEngine == "geminitts": + logging.debug('[DAEMON][GenerateTTS] TTSEngine = geminitts') + # Pas de cache pour Gemini TTS : sortie LLM non déterministe, toujours régénérer + filepath = ttsFile + logging.debug('[DAEMON][GenerateTTS] Nom du fichier à générer :: %s', filepath) + _textToSynth = ttsText + if _useAI: + ttsAIText = TTSCast.genAI(ttsText, _aiCustomSysPrompt, _aiCustomTone, _aiCustomTemp) + if ttsAIText is not None: + logging.debug('[DAEMON][GenerateTTS] Génération Gemini TTS avec IA') + _textToSynth = ttsAIText + else: + logging.warning('[DAEMON][GenerateTTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') + if myConfig.appConvertSingleQuote: + _textToSynth = Functions.convertSingleQuoteToDoubleQuote(_textToSynth) + audioBytes = TTSCast.geminiTTS(_textToSynth, ttsGeminiVoiceName, _ttsGeminiStyle) + if isinstance(audioBytes, bytes): + with open(filepath, 'wb') as f: + f.write(audioBytes) + logging.debug('[DAEMON][GenerateTTS] Fichier TTS Gemini généré :: %s', filepath) + else: + logging.warning('[DAEMON][GenerateTTS][GEMINI] Réponse invalide de l\'API Gemini TTS — voix : %s — extrait : %s', ttsGeminiVoiceName, repr(_textToSynth[:80])) + + else: + logging.error('[DAEMON][GenerateTTS] Moteur TTS inconnu ou non supporté : "%s". Valeurs acceptées : gcloudtts, gtranslatetts, jeedomtts, voicersstts, geminitts.', ttsEngine) + except Exception as e: - logging.error('[DAEMON][GenerateTTS] Exception on TTS :: %s', e) + logging.error('[DAEMON][GenerateTTS] Exception on TTS :: %s | moteur : %s | extrait : %s', e, ttsEngine, repr(ttsText[:80])) logging.debug(traceback.format_exc()) @staticmethod - def getTTS(ttsText, ttsGoogleUUID, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEngine, ttsSpeed='1.0', ttsRSSSpeed='0', ttsOptions=None, cmdNotificationId=0): + def getTTS(ttsText, ttsGoogleUUID, ttsVoiceName, ttsRSSVoiceName, ttsGeminiVoiceName, ttsLang, ttsEngine, ttsSpeed='1.0', ttsRSSSpeed='0', ttsOptions=None, cmdNotificationId=0): try: logging.debug('[DAEMON][TTS] Check des répertoires') cachePath = myConfig.ttsCacheFolderWeb @@ -829,7 +941,11 @@ def getTTS(ttsText, ttsGoogleUUID, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEn if not ttsOptions: ttsOptions = None - + + # Override moteur si Gemini TTS est le moteur par défaut + if myConfig.geminiTTSDefault and myConfig.geminiTTSEnabled: + ttsEngine = 'geminitts' + _ttsVolume = None _appDing = True _cmdWait = None @@ -843,6 +959,8 @@ def getTTS(ttsText, ttsGoogleUUID, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEn _aiReformulatedText = None _cmdOpts = {} _originalTtsText = ttsText + _ttsGeminiStyle = myConfig.geminiTTSStyle + _useStreaming = myConfig.streamingDefault # Gemini TTS uniquement — ignoré pour les autres moteurs try: if (ttsOptions is not None): @@ -863,13 +981,24 @@ def getTTS(ttsText, ttsGoogleUUID, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEn _useSSML = options_json.get('ssml', False) # Silent Before _silenceBefore = options_json.get('before', None) + # Gemini TTS Style + _ttsGeminiStyle = options_json.get('style', _ttsGeminiStyle) + # Streaming mode + _useStreaming = options_json.get('streaming', myConfig.streamingDefault) + # Engine override (par notification) + _requestedEngine = options_json.get('engine', None) + if _requestedEngine is not None: + if _requestedEngine == 'geminitts' and not myConfig.geminiTTSEnabled: + logging.error('[DAEMON][TTS] Option "engine: geminitts" refusée : Gemini TTS n\'est pas activé dans la configuration du plugin. Activez Gemini TTS dans la configuration avant d\'utiliser cette option. Aucun TTS diffusé.') + return False + ttsEngine = _requestedEngine if _silenceBefore is not None and _useSSML is False: _useSSML = True ttsText = "

" + ttsText + "

" - logging.debug('[DAEMON][TTS] Ajout de %s de silence avant le TTS :: %s', str(_silenceBefore), ttsText) + logging.info('[DAEMON][TTS] Ajout de %s de silence avant le TTS :: %s', str(_silenceBefore), ttsText) elif _silenceBefore is not None and _useSSML is True: - logging.error('[DAEMON][TTS] Les options "before" et "ssml" ne peuvent pas être utilisées dans la même commande.') + logging.error('[DAEMON][TTS] Les options "before" et "ssml" ne peuvent pas être utilisées dans la même commande. Aucun TTS diffusé.') return False # Force @@ -881,16 +1010,19 @@ def getTTS(ttsText, ttsGoogleUUID, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEn if _ttsVoiceCode is not None: if ttsEngine == "gcloudtts": ttsVoiceName = _ttsVoiceCode - logging.debug('[DAEMON][GenerateTTS] Voix Custom (Google Cloud) :: %s', ttsVoiceName) + logging.debug('[DAEMON][TTS] Voix Custom (Google Cloud) :: %s', ttsVoiceName) elif ttsEngine == "gtranslatetts": ttsLang = _ttsVoiceCode - logging.debug('[DAEMON][GenerateTTS] Voix Custom (Google Translate) :: %s', ttsLang) + logging.debug('[DAEMON][TTS] Voix Custom (Google Translate) :: %s', ttsLang) elif ttsEngine == "voicersstts": ttsRSSVoiceName = _ttsVoiceCode - logging.debug('[DAEMON][GenerateTTS] Voix Custom (VoiceRSS) :: %s', ttsRSSVoiceName) + logging.debug('[DAEMON][TTS] Voix Custom (VoiceRSS) :: %s', ttsRSSVoiceName) elif ttsEngine == "jeedomtts": ttsLang = _ttsVoiceCode - logging.debug('[DAEMON][GenerateTTS] Voix Custom (Jeedom) :: %s', ttsLang) + logging.debug('[DAEMON][TTS] Voix Custom (Jeedom) :: %s', ttsLang) + elif ttsEngine == "geminitts": + ttsGeminiVoiceName = _ttsVoiceCode + logging.debug('[DAEMON][TTS] Voix Custom (Gemini TTS) :: %s', ttsGeminiVoiceName) # Pass-through options for notification (non-daemon keys) if cmdNotificationId: @@ -898,16 +1030,19 @@ def getTTS(ttsText, ttsGoogleUUID, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEn logging.debug('[DAEMON][TTS] Notification pass-through opts :: %s', str(_cmdOpts)) logging.debug('[DAEMON][TTS] Options :: %s', str(options_json)) except ValueError as e: - logging.debug('[DAEMON][TTS] Options mal formatées (Json KO) :: %s', e) + logging.warning('[DAEMON][TTS] Options mal formatées (Json KO) :: %s', e) _appDing = False if myConfig.appDisableDing else _appDing # Texte brut (sans IA, ou SSML prioritaire sur l'IA) : mise à jour ttsLastMessage + notification si applicable if not _useAI or _useSSML: TTSCast._sendTTSResult(_originalTtsText, False, ttsGoogleUUID, cmdNotificationId, _cmdOpts) - + + if _useStreaming and ttsEngine != 'geminitts' and not myConfig.streamingDefault: + logging.warning('[DAEMON][TTS] Option "streaming" ignorée : le mode streaming n\'est supporté que par le moteur Gemini TTS (moteur actif : "%s"). Supprimez l\'option "streaming:true" de cet appel TTS.', ttsEngine) + if ttsEngine == "gcloudtts": - logging.debug('[DAEMON][TTS] TTSEngine = gcloudtts') + logging.info('[DAEMON][TTS] TTSEngine = gcloudtts') logging.debug('[DAEMON][TTS] Import de la clé API :: *** ') if myConfig.gCloudApiKey != 'noKey': gKey = os.path.join(myConfig.configFullPath, myConfig.gCloudApiKey) @@ -918,9 +1053,7 @@ def getTTS(ttsText, ttsGoogleUUID, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEn return False logging.debug('[DAEMON][TTS] Génération du fichier TTS (mp3/wav)') - # _ext = ".wav" if myConfig.gCloudAudioEncoding == "LINEAR16" else ".mp3" - # Force extension MP3 to ensure playback on all devices even if content is WAV - _ext = ".mp3" + _ext = ".wav" if myConfig.gCloudAudioEncoding == "LINEAR16" else ".mp3" raw_filename = ttsText + "|gCloudTTS|" + ttsVoiceName + "|" + ttsSpeed + "|" + str(_useSSML) + "|" + myConfig.gCloudAudioEncoding filename = hashlib.md5(raw_filename.encode('utf-8')).hexdigest() + _ext filepath = os.path.join(symLinkPath, filename) @@ -942,7 +1075,7 @@ def getTTS(ttsText, ttsGoogleUUID, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEn TTSCast._sendTTSResult(_aiReformulatedText, False, ttsGoogleUUID, cmdNotificationId, _cmdOpts) text_input = googleCloudTTS.SynthesisInput(text=ttsAIText) else: - logging.error('[DAEMON][TTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') + logging.warning('[DAEMON][TTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') TTSCast._sendTTSResult(_originalTtsText, False, ttsGoogleUUID, cmdNotificationId, _cmdOpts) if myConfig.appConvertSingleQuote: ttsText = Functions.convertSingleQuoteToDoubleQuote(ttsText) @@ -964,7 +1097,7 @@ def getTTS(ttsText, ttsGoogleUUID, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEn audio_config = googleCloudTTS.AudioConfig( audio_encoding=_audio_encoding, effects_profile_id=['medium-bluetooth-speaker-class-device'], - sample_rate_hertz=48000 if _useLinear16 else None, + sample_rate_hertz=myConfig.gCloudSampleRate if _useLinear16 else None, speaking_rate=float(ttsSpeed) ) @@ -976,28 +1109,26 @@ def getTTS(ttsText, ttsGoogleUUID, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEn with wave.open(out, 'wb') as wav_file: wav_file.setnchannels(1) # Mono wav_file.setsampwidth(2) # 16-bit - wav_file.setframerate(48000) + wav_file.setframerate(myConfig.gCloudSampleRate) wav_file.writeframes(response.audio_content) else: out.write(response.audio_content) - logging.debug('[DAEMON][TTS] Fichier TTS généré :: %s', filepath) + logging.info('[DAEMON][TTS] Fichier TTS généré :: %s', filepath) else: - logging.debug('[DAEMON][TTS] Le fichier TTS existe déjà dans le cache :: %s', filepath) + logging.info('[DAEMON][TTS] Le fichier TTS existe déjà dans le cache :: %s', filepath) urlFileToPlay = f'{ttsSrvWeb}{filename}' logging.debug('[DAEMON][TTS] URL du fichier TTS à diffuser :: %s', urlFileToPlay) - # _mimeType = "audio/wav" if myConfig.gCloudAudioEncoding == "LINEAR16" else "audio/mp3" - # Force mimetype MP3 to ensure playback on all devices even if content is WAV - _mimeType = "audio/mp3" + _mimeType = "audio/wav" if myConfig.gCloudAudioEncoding == "LINEAR16" else "audio/mp3" res = TTSCast.castToGoogleHome(urltoplay=urlFileToPlay, googleUUID=ttsGoogleUUID, volumeForPlay=_ttsVolume, appDing=_appDing, cmdWait=_cmdWait, cmdForce=_cmdForce, mimeType=_mimeType) - logging.debug('[DAEMON][TTS] Résultat de la lecture du TTS sur le Google Home :: %s', str(res)) + logging.info('[DAEMON][TTS] Résultat de la lecture du TTS sur le Google Home :: %s', str(res)) else: logging.warning('[DAEMON][TTS] Clé API invalide :: ' + myConfig.gCloudApiKey) elif ttsEngine == "gtranslatetts": - logging.debug('[DAEMON][TTS] TTSEngine = gtranslatetts') + logging.info('[DAEMON][TTS] TTSEngine = gtranslatetts') raw_filename = ttsText + "|gTTS|" + ttsLang filename = hashlib.md5(raw_filename.encode('utf-8')).hexdigest() + ".mp3" filepath = os.path.join(symLinkPath, filename) @@ -1014,7 +1145,7 @@ def getTTS(ttsText, ttsGoogleUUID, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEn TTSCast._sendTTSResult(_aiReformulatedText, False, ttsGoogleUUID, cmdNotificationId, _cmdOpts) ttsText = ttsAIText else: - logging.error('[DAEMON][TTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') + logging.warning('[DAEMON][TTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') TTSCast._sendTTSResult(_originalTtsText, False, ttsGoogleUUID, cmdNotificationId, _cmdOpts) client = gTTS(ttsText, lang=langToTTS) client.save(filepath) @@ -1024,18 +1155,18 @@ def getTTS(ttsText, ttsGoogleUUID, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEn os.remove(filepath) except OSError: pass - logging.debug('[DAEMON][TTS] Google Translate API ERROR :: %s', e) + logging.warning('[DAEMON][TTS] Google Translate API ERROR :: %s | lang : %s | extrait : %s', e, ttsLang, repr(ttsText[:80])) else: - logging.debug('[DAEMON][TTS] Le fichier TTS existe déjà dans le cache :: %s', filepath) + logging.info('[DAEMON][TTS] Le fichier TTS existe déjà dans le cache :: %s', filepath) urlFileToPlay = f'{ttsSrvWeb}{filename}' logging.debug('[DAEMON][TTS] URL du fichier TTS à diffuser :: %s', urlFileToPlay) res = TTSCast.castToGoogleHome(urltoplay=urlFileToPlay, googleUUID=ttsGoogleUUID, volumeForPlay=_ttsVolume, appDing=_appDing, cmdWait=_cmdWait) - logging.debug('[DAEMON][TTS] Résultat de la lecture du TTS sur le Google Home :: %s', str(res)) + logging.info('[DAEMON][TTS] Résultat de la lecture du TTS sur le Google Home :: %s', str(res)) elif ttsEngine == "jeedomtts": - logging.debug('[DAEMON][TTS] TTSEngine = jeedomtts') + logging.info('[DAEMON][TTS] TTSEngine = jeedomtts') raw_filename = ttsText + "|JeedomTTS|" + ttsLang filename = hashlib.md5(raw_filename.encode('utf-8')).hexdigest() + ".mp3" @@ -1051,25 +1182,25 @@ def getTTS(ttsText, ttsGoogleUUID, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEn TTSCast._sendTTSResult(_aiReformulatedText, False, ttsGoogleUUID, cmdNotificationId, _cmdOpts) ttsText = ttsAIText else: - logging.error('[DAEMON][TTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') + logging.warning('[DAEMON][TTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') TTSCast._sendTTSResult(_originalTtsText, False, ttsGoogleUUID, cmdNotificationId, _cmdOpts) ttsResult = TTSCast.jeedomTTS(ttsText, ttsLang) if ttsResult is not None: with open(filepath, 'wb') as f: f.write(ttsResult) else: - logging.debug('[DAEMON][TTS] JeedomTTS Error :: Incorrect Output') + logging.warning('[DAEMON][TTS] JeedomTTS Error :: Incorrect Output — lang : %s — extrait : %s', ttsLang, repr(ttsText[:80])) else: - logging.debug('[DAEMON][TTS] Le fichier TTS existe déjà dans le cache :: %s', filepath) + logging.info('[DAEMON][TTS] Le fichier TTS existe déjà dans le cache :: %s', filepath) urlFileToPlay = f'{ttsSrvWeb}{filename}' logging.debug('[DAEMON][TTS] URL du fichier TTS à diffuser :: %s', urlFileToPlay) res = TTSCast.castToGoogleHome(urltoplay=urlFileToPlay, googleUUID=ttsGoogleUUID, volumeForPlay=_ttsVolume, appDing=_appDing, cmdWait=_cmdWait) - logging.debug('[DAEMON][TTS] Résultat de la lecture du TTS sur le Google Home :: %s', str(res)) + logging.info('[DAEMON][TTS] Résultat de la lecture du TTS sur le Google Home :: %s', str(res)) elif ttsEngine == "voicersstts": - logging.debug('[DAEMON][TTS] TTSEngine = voicersstts') + logging.info('[DAEMON][TTS] TTSEngine = voicersstts') logging.debug('[DAEMON][TTS] Import de la clé API :: *** ') if myConfig.apiRSSKey != 'noKey': raw_filename = ttsText + "|VoiceRSSTTS|" + ttsRSSVoiceName + "|" + ttsRSSSpeed + "|" + str(_useSSML) @@ -1086,31 +1217,97 @@ def getTTS(ttsText, ttsGoogleUUID, ttsVoiceName, ttsRSSVoiceName, ttsLang, ttsEn TTSCast._sendTTSResult(_aiReformulatedText, False, ttsGoogleUUID, cmdNotificationId, _cmdOpts) ttsText = ttsAIText else: - logging.error('[DAEMON][TTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') + logging.warning('[DAEMON][TTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') TTSCast._sendTTSResult(_originalTtsText, False, ttsGoogleUUID, cmdNotificationId, _cmdOpts) ttsResult = TTSCast.voiceRSS(ttsText, ttsRSSVoiceName, ttsRSSSpeed, _useSSML) if ttsResult is not None: with open(filepath, 'wb') as f: f.write(ttsResult) else: - logging.debug('[DAEMON][TTS] VoiceRSS Error :: Incorrect Output') + logging.warning('[DAEMON][TTS] VoiceRSS Error :: Incorrect Output — voix : %s — extrait : %s', ttsRSSVoiceName, repr(ttsText[:80])) else: - logging.debug('[DAEMON][TTS] Le fichier TTS existe déjà dans le cache :: %s', filepath) + logging.info('[DAEMON][TTS] Le fichier TTS existe déjà dans le cache :: %s', filepath) urlFileToPlay = f'{ttsSrvWeb}{filename}' logging.debug('[DAEMON][TTS] URL du fichier TTS à diffuser :: %s', urlFileToPlay) res = TTSCast.castToGoogleHome(urltoplay=urlFileToPlay, googleUUID=ttsGoogleUUID, volumeForPlay=_ttsVolume, appDing=_appDing, cmdWait=_cmdWait) - logging.debug('[DAEMON][TTS] Résultat de la lecture du TTS sur le Google Home :: %s', str(res)) + logging.info('[DAEMON][TTS] Résultat de la lecture du TTS sur le Google Home :: %s', str(res)) else: logging.warning('[DAEMON][TTS] Clé API (Voice RSS) invalide :: ' + myConfig.apiRSSKey) + elif ttsEngine == "geminitts": + logging.info('[DAEMON][TTS] TTSEngine = geminitts') + # Pas de cache pour Gemini TTS : sortie LLM non déterministe, toujours régénérer + raw_filename = ttsText + "|GeminiTTS|" + ttsGeminiVoiceName + "|" + myConfig.geminiTTSModel + filename = hashlib.md5(raw_filename.encode('utf-8')).hexdigest() + ".wav" + filepath = os.path.join(symLinkPath, filename) + logging.debug('[DAEMON][TTS] Nom du fichier à générer :: %s', filepath) + _textToSynth = ttsText + if _useAI: + ttsAIText = TTSCast.genAI(ttsText, _aiCustomSysPrompt, _aiCustomTone, _aiCustomTemp) + if ttsAIText is not None: + logging.debug('[DAEMON][TTS] Génération Gemini TTS avec IA') + _aiReformulatedText = ttsAIText + TTSCast._sendTTSResult(_aiReformulatedText, False, ttsGoogleUUID, cmdNotificationId, _cmdOpts) + _textToSynth = ttsAIText + else: + logging.warning('[DAEMON][TTS] Erreur lors de la génération du TTS avec IA. Génération du TTS sans IA (Backup)') + TTSCast._sendTTSResult(_originalTtsText, False, ttsGoogleUUID, cmdNotificationId, _cmdOpts) + if myConfig.appConvertSingleQuote: + _textToSynth = Functions.convertSingleQuoteToDoubleQuote(_textToSynth) + + if _useStreaming: + logging.debug('[DAEMON][TTS] Mode streaming activé pour Gemini TTS') + + # Pré-lecture du premier chunk : détection du format audio réel avant de lancer le Chromecast + _t = time.time() + logging.info('[TIMING][GeminiStream] t0_start :: %.3f (%s)', _t, datetime.datetime.fromtimestamp(_t).strftime('%H:%M:%S.') + f'{int((_t % 1) * 1000):03d}') + prefetch = TTSCast.geminiTTS(_textToSynth, ttsGeminiVoiceName, _ttsGeminiStyle, streaming=True) + if prefetch is None: + logging.error('[DAEMON][TTS] GeminiTTS streaming :: échec de la pré-lecture | voix : %s | extrait : %s', ttsGeminiVoiceName, repr(_textToSynth[:80])) + return False + streamIter, firstChunkBytes, sampleRate, channels, streamClient = prefetch + mimeType = 'audio/wav' # Le proxy envoie un stream WAV RIFF (PCM LE 16-bit) + logging.debug('[DAEMON][TTS] Format stream détecté :: %s', mimeType) + + streamDir = myConfig.ttsStreamFolderTmp + os.makedirs(streamDir, exist_ok=True) + pipeName = str(uuid4()) + '.l16' + pipePath = os.path.join(streamDir, pipeName) + os.mkfifo(pipePath) # type: ignore[attr-defined] # POSIX only — cible Debian + pipeUrl = f'{myConfig.ttsWebSrvMediaProxy}?type=stream&file={pipeName}&rate={sampleRate}&channels={channels}' + logging.debug('[DAEMON][TTS] Pipe créé :: %s | URL :: %s', pipePath, pipeUrl) + + threading.Thread(target=TTSCast.geminiTTSStream, args=[streamIter, firstChunkBytes, sampleRate, channels, pipePath, filepath, streamClient]).start() + _t = time.time() + logging.info('[TIMING][GeminiStream] t1_castStart :: %.3f (%s)', _t, datetime.datetime.fromtimestamp(_t).strftime('%H:%M:%S.') + f'{int((_t % 1) * 1000):03d}') + res = TTSCast.castToGoogleHome(urltoplay=pipeUrl, googleUUID=ttsGoogleUUID, volumeForPlay=_ttsVolume, appDing=_appDing, cmdWait=_cmdWait, cmdForce=_cmdForce, mimeType=mimeType, streamType='LIVE') + + else: + audioBytes = TTSCast.geminiTTS(_textToSynth, ttsGeminiVoiceName, _ttsGeminiStyle) + if isinstance(audioBytes, bytes): + with open(filepath, 'wb') as f: + f.write(audioBytes) + logging.info('[DAEMON][TTS] Fichier TTS Gemini généré :: %s', filepath) + else: + logging.error('[DAEMON][TTS] GeminiTTS Error :: Incorrect Output — voix : %s — extrait : %s', ttsGeminiVoiceName, repr(_textToSynth[:80])) + return False + urlFileToPlay = f'{ttsSrvWeb}{filename}' + logging.debug('[DAEMON][TTS] URL du fichier TTS à diffuser :: %s', urlFileToPlay) + res = TTSCast.castToGoogleHome(urltoplay=urlFileToPlay, googleUUID=ttsGoogleUUID, volumeForPlay=_ttsVolume, appDing=_appDing, cmdWait=_cmdWait, cmdForce=_cmdForce, mimeType='audio/wav') + + logging.info('[DAEMON][TTS] Résultat de la lecture du TTS sur le Google Home :: %s', str(res)) + + else: + logging.error('[DAEMON][TTS] Moteur TTS inconnu ou non supporté : "%s". Valeurs acceptées : gcloudtts, gtranslatetts, jeedomtts, voicersstts, geminitts.', ttsEngine) + except Exception as e: - logging.error('[DAEMON][TTS] Exception on TTS :: %s', e) + logging.error('[DAEMON][TTS] Exception on TTS :: %s | moteur : %s | extrait : %s', e, ttsEngine, repr(ttsText[:80])) logging.debug(traceback.format_exc()) @staticmethod - def castToGoogleHome(urltoplay, googleName='', googleUUID='', volumeForPlay=None, appDing=True, cmdWait=None, cmdForce=False, mimeType='audio/mp3'): + def castToGoogleHome(urltoplay, googleName='', googleUUID='', volumeForPlay=None, appDing=True, cmdWait=None, cmdForce=False, mimeType='audio/mp3', streamType='BUFFERED'): if googleName != '': logging.debug('[DAEMON][Cast] Diffusion (Test) sur le Google Home :: %s', googleName) @@ -1123,7 +1320,7 @@ def castToGoogleHome(urltoplay, googleName='', googleUUID='', volumeForPlay=None try: chromecasts = [mycast for mycast in myConfig.NETCAST_DEVICES.values() if mycast.name == googleName] if not chromecasts: - logging.debug('[DAEMON][Cast] Aucun Chromecast avec ce nom :: %s', googleName) + logging.warning('[DAEMON][Cast] Aucun Chromecast avec ce nom :: %s', googleName) return False cast = chromecasts[0] logging.debug('[DAEMON][Cast] Chromecast trouvé, tentative de lecture TTS') @@ -1156,8 +1353,7 @@ def castToGoogleHome(urltoplay, googleName='', googleUUID='', volumeForPlay=None app_data = { "media_id": urltoplay, "media_type": mimeType, - "stream_type": "BUFFERED", - # "stream_type": "LIVE", + "stream_type": streamType, "title": "TTSCast", "thumb": urlThumb, "metadata": { @@ -1183,7 +1379,7 @@ def castToGoogleHome(urltoplay, googleName='', googleUUID='', volumeForPlay=None cast.media_controller.block_until_active() - logging.debug('[DAEMON][Cast] Diffusion lancée :: %s', str(cast.media_controller.status)) + logging.info('[DAEMON][Cast] Diffusion lancée :: %s', str(cast.media_controller.status)) media_player_state = None media_has_played = False @@ -1211,7 +1407,7 @@ def castToGoogleHome(urltoplay, googleName='', googleUUID='', volumeForPlay=None chromecasts = None return True except Exception as e: - logging.debug('[DAEMON][Cast] Exception (Chromecasts) :: %s', e) + logging.warning('[DAEMON][Cast] Exception (Chromecasts) :: %s', e) logging.debug(traceback.format_exc()) if volumeBeforePlay is not None: @@ -1243,7 +1439,7 @@ def castToGoogleHome(urltoplay, googleName='', googleUUID='', volumeForPlay=None cast = myConfig.NETCAST_DEVICES[_uuid] logging.debug('[DAEMON][Cast] Chromecast trouvé, tentative de lecture TTS') else: - logging.debug('[DAEMON][Cast] Aucun Chromecast avec cet UUID nom :: %s', googleUUID) + logging.warning('[DAEMON][Cast] Aucun Chromecast avec cet UUID :: %s', googleUUID) return False # --- Wait Queue Management (Entrance) --- @@ -1281,8 +1477,7 @@ def castToGoogleHome(urltoplay, googleName='', googleUUID='', volumeForPlay=None app_data = { "media_id": urltoplay, "media_type": mimeType, - "stream_type": "BUFFERED", - # "stream_type": "LIVE", + "stream_type": streamType, "title": "TTSCast", "thumb": urlThumb, "metadata": { @@ -1309,7 +1504,7 @@ def castToGoogleHome(urltoplay, googleName='', googleUUID='', volumeForPlay=None cast.media_controller.block_until_active() - logging.debug('[DAEMON][Cast] Diffusion lancée :: %s', str(cast.media_controller.status)) + logging.info('[DAEMON][Cast] Diffusion lancée :: %s', str(cast.media_controller.status)) media_player_state = None media_has_played = False @@ -1339,7 +1534,7 @@ def castToGoogleHome(urltoplay, googleName='', googleUUID='', volumeForPlay=None cast = None return True except Exception as e: - logging.debug('[DAEMON][Cast] Exception (Chromecasts) :: %s', e) + logging.warning('[DAEMON][Cast] Exception (Chromecasts) :: %s', e) logging.debug(traceback.format_exc()) if volumeBeforePlay is not None: @@ -1380,15 +1575,16 @@ def genAI(_aiPrompt, _aiCustomSysPrompt=None, _aiCustomTone=None, _aiCustomTemp= project=myConfig.aiProjectID, location="global", credentials=credentials, + http_options=types.HttpOptions(timeout=30000), ) else: - logging.error('[DAEMON][genAI] Impossible de charger le fichier JSON (clé API : KO) :: %s', gKey) + logging.warning('[DAEMON][genAI] Impossible de charger le fichier JSON (clé API : KO) :: %s', gKey) return None elif myConfig.aiAuthMode == 'apikey' and myConfig.aiApiKey != 'noKey': logging.debug('[DAEMON][genAI] Chargement du moteur IA en utilisant la clé API :: %s', "***" if myConfig.aiApiKey else "N/A") - client = genai.Client(api_key=myConfig.aiApiKey) + client = genai.Client(api_key=myConfig.aiApiKey, http_options=types.HttpOptions(timeout=30000)) else: - logging.error('[DAEMON][genAI] Mode d\'authentification invalide ou clé API manquante.') + logging.warning('[DAEMON][genAI] Mode d\'authentification invalide ou clé API manquante (mode configuré : %s).', myConfig.aiAuthMode) return None logging.debug('[DAEMON][genAI] Reformulation de texte via IA') @@ -1477,12 +1673,12 @@ def genAI(_aiPrompt, _aiCustomSysPrompt=None, _aiCustomTone=None, _aiCustomTemp= Comm.sendToJeedom.add_changes('aiStats::TTSCast_AI_Stats', data) # type: ignore logging.debug('[DAEMON][GenAI][TOKENS] Stats envoyées à Jeedom') except Exception as e: - logging.error('[DAEMON][GenAI][TOKENS] Erreur lors de l\'envoi des tokens à Jeedom: %s', e) + logging.warning('[DAEMON][GenAI][TOKENS] Erreur lors de l\'envoi des tokens à Jeedom: %s', e) else: logging.warning('[DAEMON][GenAI][TOKENS] Aucun token reçu (input=0) - stats non envoyées à Jeedom') if not response.text: - logging.warning('[DAEMON][GenAI] Aucune réponse générée par Gemini.') + logging.warning('[DAEMON][GenAI] Aucune réponse générée par Gemini — extrait : %s', repr(_aiPrompt[:80])) return None else: raw_text = response.text.strip() @@ -1491,12 +1687,227 @@ def genAI(_aiPrompt, _aiCustomSysPrompt=None, _aiCustomTone=None, _aiCustomTemp= logging.debug('[DAEMON][GenAI] Réponse après nettoyage Markdown (repr) :: %s', repr(clean_text)) return clean_text else: - logging.error('[DAEMON][GenAI] Clé (JSON ou Api) et/ou ID de projet Google invalide :: %s, %s, %s', myConfig.gCloudApiKey, "***" if myConfig.aiApiKey else "N/A", myConfig.aiProjectID) + logging.warning('[DAEMON][GenAI] Clé (JSON ou Api) et/ou ID de projet Google invalide :: %s, %s, %s', myConfig.gCloudApiKey, "***" if myConfig.aiApiKey else "N/A", myConfig.aiProjectID) return None except Exception as e: - logging.error('[DAEMON][GenAI] Erreur: %s', e) + logging.error('[DAEMON][GenAI] Erreur :: %s | modèle : %s | extrait : %s', e, myConfig.aiModel, repr(_aiPrompt[:80])) return None + @staticmethod + def geminiTTS(ttsText: str, voiceName: str, style: str = '', streaming: bool = False): + """ + Génère l'audio TTS via l'API Gemini. + - streaming=False (défaut) : bloquant, retourne les bytes WAV complets ou None. + - streaming=True : pré-lit le premier chunk pour détecter le format audio réel, + retourne (stream_iterator, first_chunk_bytes, sampleRate, channels) ou None. + Utilisé exclusivement par le mode streaming Gemini TTS dans getTTS. + """ + try: + # ── Authentification ────────────────────────────────────────────── + if not ((myConfig.gCloudApiKey != 'noKey' and myConfig.aiProjectID != 'noProjectID') or myConfig.aiApiKey != 'noKey'): + logging.warning('[DAEMON][GeminiTTS] Clé API IA non configurée.') + return None + + if myConfig.aiAuthMode == 'oauth2' and myConfig.gCloudApiKey != 'noKey': + gKey = os.path.join(myConfig.configFullPath, myConfig.gCloudApiKey) + if not os.path.exists(gKey): + logging.error('[DAEMON][GeminiTTS] Impossible de charger le fichier JSON :: %s', gKey) + return None + logging.debug('[DAEMON][GeminiTTS] Chargement des credentials OAuth2 :: %s', myConfig.gCloudApiKey) + credentials = service_account.Credentials.from_service_account_file(gKey, scopes=myConfig.aiScopes) + client = genai.Client(vertexai=True, project=myConfig.aiProjectID, location='global', credentials=credentials, http_options=types.HttpOptions(timeout=30000)) + elif myConfig.aiAuthMode == 'apikey' and myConfig.aiApiKey != 'noKey': + logging.debug('[DAEMON][GeminiTTS] Client API key :: ***') + client = genai.Client(api_key=myConfig.aiApiKey, http_options=types.HttpOptions(timeout=30000)) + else: + logging.error('[DAEMON][GeminiTTS] Mode auth invalide ou clé manquante (mode configuré : %s).', myConfig.aiAuthMode) + return None + + # ── Prompt & configuration audio ────────────────────────────────── + # Le délimiteur ### TRANSCRIPT indique au modèle où commence le texte + # à synthétiser, évitant qu'il lise les instructions de style à voix haute. + prompt = f"{style}\n\n### TRANSCRIPT\n{ttsText}" if style else ttsText + genConfig = types.GenerateContentConfig( + response_modalities=['AUDIO'], + speech_config=types.SpeechConfig( + voice_config=types.VoiceConfig( + prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=voiceName) + ) + ) + ) + logging.debug('[DAEMON][GeminiTTS] Modèle :: %s | Voix :: %s | Style :: %s | Mode :: %s', + myConfig.geminiTTSModel, voiceName, style if style else 'N/A', + 'stream' if streaming else 'buffered') + + if streaming: + # ── Mode streaming : pré-lecture du premier chunk ───────────── + streamIterator = client.models.generate_content_stream( + model=myConfig.geminiTTSModel, + contents=prompt, + config=genConfig, + ) + for chunk in streamIterator: + if (chunk.candidates + and (content := chunk.candidates[0].content) + and content.parts + and (blob := content.parts[0].inline_data) + and blob.data): + mime_type = blob.mime_type or '' + rate_match = re.search(r'rate=(\d+)', mime_type) + channels_match = re.search(r'channels=(\d+)', mime_type) + sampleRate = int(rate_match.group(1)) if rate_match else 24000 + channels = int(channels_match.group(1)) if channels_match else 1 + logging.debug('[DAEMON][GeminiTTS] Premier chunk stream :: rate=%d | channels=%d', sampleRate, channels) + return streamIterator, blob.data, sampleRate, channels, client + logging.error('[DAEMON][GeminiTTS] Streaming :: aucun chunk audio reçu | voix : %s | extrait : %s', voiceName, repr(ttsText[:80])) + return None + + else: + # ── Mode buffered : réponse complète, encapsulation WAV ─────── + response = client.models.generate_content( + model=myConfig.geminiTTSModel, + contents=prompt, + config=genConfig, + ) + if hasattr(response, 'usage_metadata') and response.usage_metadata: + usage = response.usage_metadata + input_tokens = getattr(usage, 'prompt_token_count', 0) or 0 + output_tokens = getattr(usage, 'candidates_token_count', 0) or 0 + total_tokens = getattr(usage, 'total_token_count', 0) or 0 + logging.info('[DAEMON][GeminiTTS][TOKENS] Model: %s | Input: %d | Output: %d | Total: %d', + myConfig.geminiTTSModel, input_tokens, output_tokens, total_tokens) + if (response.candidates + and (content := response.candidates[0].content) + and content.parts + and (blob := content.parts[0].inline_data) + and blob.data): + audio_data = blob.data + mime_type = blob.mime_type or '' + logging.debug('[DAEMON][GeminiTTS] Audio généré :: %d bytes | mime_type :: %s', len(audio_data), mime_type) + # L'API Gemini TTS retourne du PCM brut (ex: "audio/l16; rate=24000; channels=1") + if 'wav' not in mime_type.lower(): + rate_match = re.search(r'rate=(\d+)', mime_type) + channels_match = re.search(r'channels=(\d+)', mime_type) + sample_rate = int(rate_match.group(1)) if rate_match else 24000 + channels = int(channels_match.group(1)) if channels_match else 1 + buf = io.BytesIO() + with wave.open(buf, 'wb') as wf: + wf.setnchannels(channels) + wf.setsampwidth(2) # 16-bit signed little-endian (PCM L16) + wf.setframerate(sample_rate) + wf.writeframes(audio_data) + audio_data = buf.getvalue() + logging.debug('[DAEMON][GeminiTTS] PCM encapsulé en WAV :: %d bytes | rate :: %d Hz | channels :: %d', + len(audio_data), sample_rate, channels) + else: + logging.debug('[DAEMON][GeminiTTS] Audio WAV natif retourné :: %d bytes', len(audio_data)) + return audio_data + logging.warning('[DAEMON][GeminiTTS] Aucune donnée audio dans la réponse | voix : %s | extrait : %s', voiceName, repr(ttsText[:80])) + return None + + except Exception as e: + logging.error('[DAEMON][GeminiTTS] Exception :: %s | Modèle : %s | Voix : %s | Mode : %s | Extrait : %s', e, myConfig.geminiTTSModel, voiceName, 'stream' if streaming else 'buffered', repr(ttsText[:80])) + logging.debug(traceback.format_exc()) + return None + + @staticmethod + def geminiTTSStream(streamIterator, firstChunkBytes: bytes, sampleRate: int, channels: int, pipePath: str, cacheFilePath: str = '', _client=None): # noqa: ARG004 — _client maintenu en vie pour éviter la fermeture des sockets httpx + """ + Écrit le stream TTS Gemini dans le named pipe (mkfifo). + Reçoit l'itérateur de stream déjà initié et le premier chunk pré-lu. + Construit le WAV pour la mise en cache si cacheFilePath fourni. + """ + fd = None + pcmBuffer = [firstChunkBytes] + try: + # Ouvrir le pipe en écriture non-bloquant avec retry 10 s + deadline = time.time() + 10.0 + while time.time() < deadline: + try: + fd = os.open(pipePath, os.O_WRONLY | os.O_NONBLOCK) # type: ignore[attr-defined] # POSIX only — cible Debian + break + except OSError as oe: + if oe.errno == errno.ENXIO: + time.sleep(0.005) # 5ms — réduit la latence avant la première écriture dans le pipe + fd = None + continue + raise + if fd is None: + logging.error('[DAEMON][GeminiTTSStream] Timeout (10s) : aucun lecteur sur le pipe :: %s', pipePath) + return None + + # Retirer O_NONBLOCK après l'ouverture : les écritures doivent bloquer si le buffer est plein + os.set_blocking(fd, True) # type: ignore[attr-defined] # POSIX only — cible Debian + logging.debug('[DAEMON][GeminiTTSStream] Pipe ouvert en écriture, démarrage du streaming') + + # Header WAV RIFF pour stream de durée inconnue + # audio/L16 (RFC 2586) attend du big-endian, mais Gemini retourne du PCM little-endian. + # audio/wav accepte du PCM LE nativement — patch des tailles à 0x7FFFFFFF pour stream ouvert. + _wavBuf = io.BytesIO() + with wave.open(_wavBuf, 'wb') as _wh: + _wh.setnchannels(channels) + _wh.setsampwidth(2) + _wh.setframerate(sampleRate) + _wh.writeframes(b'') + _wavHeader = bytearray(_wavBuf.getvalue()) + _wavHeader[4:8] = b'\xff\xff\xff\x7f' # RIFF chunk size : durée inconnue + _wavHeader[40:44] = b'\xff\xff\xff\x7f' # data chunk size : durée inconnue + os.write(fd, bytes(_wavHeader)) + + # Écrire le premier chunk déjà pré-lu + os.write(fd, firstChunkBytes) + + # Continuer l'itération du stream + for chunk in streamIterator: + if (chunk.candidates + and (content := chunk.candidates[0].content) + and content.parts + and (blob := content.parts[0].inline_data) + and blob.data): + pcmBuffer.append(blob.data) + os.write(fd, blob.data) + + os.close(fd) + fd = None + logging.debug('[DAEMON][GeminiTTSStream] Streaming PCM terminé :: %d chunks', len(pcmBuffer)) + + # Construire le WAV pour la mise en cache + allPcm = b''.join(pcmBuffer) + buf = io.BytesIO() + with wave.open(buf, 'wb') as wf: + wf.setnchannels(channels) + wf.setsampwidth(2) + wf.setframerate(sampleRate) + wf.writeframes(allPcm) + wavBytes = buf.getvalue() + logging.debug('[DAEMON][GeminiTTSStream] WAV cache :: %d bytes | rate=%d | channels=%d', len(wavBytes), sampleRate, channels) + if cacheFilePath: + with open(cacheFilePath, 'wb') as f: + f.write(wavBytes) + logging.debug('[DAEMON][GeminiTTSStream] Fichier TTS Gemini stream mis en cache :: %s', cacheFilePath) + _t = time.time() + logging.info('[TIMING][GeminiStream] t2_cacheWritten :: %.3f (%s)', _t, datetime.datetime.fromtimestamp(_t).strftime('%H:%M:%S.') + f'{int((_t % 1) * 1000):03d}') + return wavBytes + except BrokenPipeError: + # Le client (Chromecast via proxy PHP) a fermé la connexion avant la fin du stream + # Ce comportement est attendu si le Chromecast déconnecte (LOAD_FAILED, stop, etc.) + logging.warning('[DAEMON][GeminiTTSStream] Client déconnecté avant la fin du stream (Broken pipe)') + return None + except Exception as e: + logging.error('[DAEMON][GeminiTTSStream] Exception :: %s | chunks reçus : %d', e, len(pcmBuffer)) + logging.debug(traceback.format_exc()) + return None + finally: + if fd is not None: + try: + os.close(fd) + except Exception: + pass + try: + os.unlink(pipePath) + except Exception: + pass + @staticmethod def handleAiReformat(message): """ @@ -1678,7 +2089,7 @@ def _setVol(uuid_dev, vol): dev.set_volume(vol) logging.debug(f'[DAEMON][GroupVol] Set {vol} on {dev.name}') except Exception as ex: - logging.error(f'[DAEMON][GroupVol] Error on {uuid_dev}: {ex}') + logging.error('[DAEMON][GroupVol] Error on %s :: %s', uuid_dev, ex) threads = [] for member_uuid in membersUUIDs: @@ -1703,7 +2114,7 @@ def _resVol(uuid_dev, vol): dev.set_volume(vol) logging.debug(f'[DAEMON][GroupVol] Restored {vol} on {dev.name}') except Exception as ex: - logging.error(f'[DAEMON][GroupVol] Error restoring {uuid_dev}: {ex}') + logging.error('[DAEMON][GroupVol] Error restoring %s :: %s', uuid_dev, ex) threads = [] for m_uuid, m_vol in volumeSnapshot.items(): @@ -1866,7 +2277,7 @@ def controllerStartApp(cast, _googleUUID, _value, _options): _cmdWait = options_json.get('wait', None) logging.debug(f'[DAEMON][controllerActions] StartApp :: Options :: {str(options_json)}') except ValueError as e: - logging.debug(f'[DAEMON][controllerActions] StartApp :: Options mal formatées (Json KO) :: {e}') + logging.warning(f'[DAEMON][controllerActions] StartApp :: Options mal formatées (Json KO) :: {e}') _appDing = False if myConfig.appDisableDing else _appDing @@ -1962,7 +2373,7 @@ def controllerYoutube(cast, _googleUUID, _value, _options): _cmdWait = options_json.get('wait', None) logging.debug(f'[DAEMON][controllerActions] YouTube :: Options :: {str(options_json)}') except ValueError as e: - logging.debug(f'[DAEMON][controllerActions] YouTube :: Options mal formatées (Json KO) :: {e}') + logging.warning(f'[DAEMON][controllerActions] YouTube :: Options mal formatées (Json KO) :: {e}') _appDing = False if myConfig.appDisableDing else _appDing @@ -2022,7 +2433,7 @@ def controllerYoutube(cast, _googleUUID, _value, _options): cast.media_controller.block_until_active() - logging.debug(f'[DAEMON][controllerActions] YouTube :: Diffusion lancée :: {str(cast.media_controller.status)}') + logging.info(f'[DAEMON][controllerActions] YouTube :: Diffusion lancée :: {cast.name} | ID: {_value}') return True except Exception as e: @@ -2058,7 +2469,7 @@ def controllerDashCast(cast, _googleUUID, _value, _options): _reload_seconds = options_json.get('reload_seconds', None) _cmdWait = options_json.get('wait', None) except ValueError as e: - logging.debug(f'[DAEMON][controllerActions] DashCast :: Options mal formatées (Json KO) :: {e}') + logging.warning(f'[DAEMON][controllerActions] DashCast :: Options mal formatées (Json KO) :: {e}') try: # Default target is the googleUUID until resolved by Queue logic @@ -2131,7 +2542,7 @@ def controllerRadios(cast, _googleUUID, _value, _options, _controller): _cmdWait = options_json.get('wait', None) logging.debug(f'[DAEMON][controllerActions] {radioType} :: Options :: {str(options_json)}') except ValueError as e: - logging.debug(f'[DAEMON][controllerActions] {radioType} :: Options mal formatées (Json KO) :: {e}') + logging.warning(f'[DAEMON][controllerActions] {radioType} :: Options mal formatées (Json KO) :: {e}') _appDing = False if myConfig.appDisableDing else _appDing @@ -2216,7 +2627,7 @@ def controllerRadios(cast, _googleUUID, _value, _options, _controller): cast.media_controller.block_until_active() - logging.debug(f'[DAEMON][controllerActions] Diffusion {radioType} lancée :: {str(cast.media_controller.status)}') + logging.info(f'[DAEMON][controllerActions] Diffusion {radioType} lancée :: {cast.name} | {radioTitle}') return True except Exception as e: @@ -2256,7 +2667,7 @@ def controllerSounds(cast, _googleUUID, _value, _options, _controller): _cmdForce = options_json.get('force', False) logging.debug(f'[DAEMON][controllerActions] {soundType} :: Options :: {str(options_json)}') except ValueError as e: - logging.debug(f'[DAEMON][controllerActions] {soundType} :: Options mal formatées (Json KO) :: {e}') + logging.warning(f'[DAEMON][controllerActions] {soundType} :: Options mal formatées (Json KO) :: {e}') _appDing = False if myConfig.appDisableDing else _appDing @@ -2296,9 +2707,9 @@ def controllerSounds(cast, _googleUUID, _value, _options, _controller): cast.set_volume(volume=_volume / 100) if (_controller == 'customsounds'): - soundURL = f'{myConfig.ttsWebSrvMedia}custom/{_value}' + soundURL = f'{myConfig.ttsWebSrvMediaProxy}?type=customsounds&file={quote(_value, safe="")}' else: - soundURL = f'{myConfig.ttsWebSrvMedia}{_value}' + soundURL = f'{myConfig.ttsWebSrvMediaProxy}?type=sounds&file={quote(_value, safe="")}' logging.debug(f'[DAEMON][controllerActions] {soundType} :: FilePath :: {soundURL}') soundThumb = f'{myConfig.ttsWebSrvImages}tts.png' @@ -2306,11 +2717,18 @@ def controllerSounds(cast, _googleUUID, _value, _options, _controller): soundTitle = f"TTSCast {soundType}" soundArtist = _value + _soundMimeTypes = { + 'mp3': 'audio/mp3', 'wav': 'audio/wav', + 'ogg': 'audio/ogg', 'opus': 'audio/ogg; codecs=opus', 'flac': 'audio/flac' + } + _ext = os.path.splitext(_value)[1].lstrip('.').lower() + soundMimeType = _soundMimeTypes.get(_ext, 'audio/mp3') + app_name = "default_media_receiver" # app_name = "bubbleupnp" app_data = { "media_id": soundURL, - "media_type": "audio/mp3", + "media_type": soundMimeType, "stream_type": "BUFFERED", "title": soundTitle, "thumb": soundThumb, @@ -2338,7 +2756,7 @@ def controllerSounds(cast, _googleUUID, _value, _options, _controller): cast.media_controller.block_until_active() - logging.debug(f'[DAEMON][controllerActions] Diffusion {soundType} lancée :: {str(cast.media_controller.status)}') + logging.info(f'[DAEMON][controllerActions] Diffusion {soundType} lancée :: {cast.name} | {_value}') media_player_state = None media_has_played = False @@ -2422,7 +2840,7 @@ def controllerMedia(cast, _googleUUID, _value, _options): logging.debug(f'[DAEMON][controllerActions] Media :: Options :: {str(options_json)}') except ValueError as e: - logging.debug(f'[DAEMON][controllerActions] Media :: Options mal formatées (Json KO) :: {e}') + logging.warning(f'[DAEMON][controllerActions] Media :: Options mal formatées (Json KO) :: {e}') _appDing = False if myConfig.appDisableDing else _appDing @@ -2514,7 +2932,7 @@ def controllerMedia(cast, _googleUUID, _value, _options): cast.media_controller.block_until_active() - logging.debug(f'[DAEMON][controllerActions] Diffusion Media lancée :: {str(cast.media_controller.status)}') + logging.info(f'[DAEMON][controllerActions] Diffusion Media lancée :: {cast.name} | {_value}') return True @@ -2880,7 +3298,7 @@ def castRemove(chromecast=None, uuid=None): logging.error('[DAEMON][NETCAST][CastRemove][Cast] Exception (%s) :: %s', str(chromecast.name), e) logging.debug(traceback.format_exc()) else: - logging.warning('[DAEMON][NETCAST][CastRemove] Chromecast with name :: %s :: Status Listener already deleted', str(chromecast.name)) + logging.debug('[DAEMON][NETCAST][CastRemove] Chromecast with name :: %s :: Status Listener already deleted', str(chromecast.name)) if (uuid in myConfig.LISTENER_MEDIA): try: @@ -2890,7 +3308,7 @@ def castRemove(chromecast=None, uuid=None): logging.error('[DAEMON][NETCAST][CastRemove][Media] Exception (%s) :: %s', str(chromecast.name), e) logging.debug(traceback.format_exc()) else: - logging.warning('[DAEMON][NETCAST][CastRemove] Chromecast with name :: %s :: Media Listener already deleted', str(chromecast.name)) + logging.debug('[DAEMON][NETCAST][CastRemove] Chromecast with name :: %s :: Media Listener already deleted', str(chromecast.name)) if (uuid in myConfig.LISTENER_CONNECT): try: @@ -2900,7 +3318,7 @@ def castRemove(chromecast=None, uuid=None): logging.error('[DAEMON][NETCAST][CastRemove][Connect] Exception (%s) :: %s', str(chromecast.name), e) logging.debug(traceback.format_exc()) else: - logging.warning('[DAEMON][NETCAST][CastRemove] Chromecast with name :: %s :: Connect Listener already deleted', str(chromecast.name)) + logging.debug('[DAEMON][NETCAST][CastRemove] Chromecast with name :: %s :: Connect Listener already deleted', str(chromecast.name)) if (uuid in myConfig.NETCAST_GROUPS): try: @@ -3199,6 +3617,7 @@ def shutdown(): parser = argparse.ArgumentParser(description='TTSCast Daemon for Jeedom plugin') parser.add_argument("--loglevel", help="Log Level for the daemon", type=str) +parser.add_argument("--castloglevel", help="Log Level for Chromecast libraries (pychromecast, zeroconf)", type=str, default='daemon') parser.add_argument("--pluginversion", help="Plugin Version", type=str) parser.add_argument("--callback", help="Callback", type=str) parser.add_argument("--apikey", help="ApiKey", type=str) @@ -3223,10 +3642,17 @@ def shutdown(): parser.add_argument("--aiusecustomsysprompt", help="Use Custom System Prompt for AI", type=str, default='0') parser.add_argument("--aicustomsysprompt", help="Custom System Prompt for AI", type=str, default='NoCustomSysPrompt') parser.add_argument("--aidefaulttone", help="Default AI Tone", type=str, default='NoDefaultTone') +parser.add_argument("--geminittsenabled", help="Enable Gemini TTS engine", type=str, default='0') +parser.add_argument("--geminittsmodel", help="Gemini TTS Model", type=str, default='noModel') +parser.add_argument("--geminittsdefault", help="Use Gemini TTS as default engine", type=str, default='0') +parser.add_argument("--geminittsstyle", help="Default style for Gemini TTS", type=str, default='') +parser.add_argument("--streamingdefault", help="Enable streaming TTS by default", type=str, default='0') args = parser.parse_args() if args.loglevel: myConfig.logLevel = args.loglevel +if args.castloglevel: + myConfig.castLogLevel = args.castloglevel if args.pluginversion: myConfig.pluginVersion = args.pluginversion if args.callback: @@ -3290,6 +3716,16 @@ def shutdown(): myConfig.aiUseCustomSysPrompt = True if args.aicustomsysprompt and args.aicustomsysprompt != 'NoCustomSysPrompt': myConfig.aiCustomSysPrompt = args.aicustomsysprompt +if args.geminittsenabled: + myConfig.geminiTTSEnabled = args.geminittsenabled != '0' +if args.geminittsmodel and args.geminittsmodel != 'noModel': + myConfig.geminiTTSModel = args.geminittsmodel +if args.geminittsdefault: + myConfig.geminiTTSDefault = args.geminittsdefault != '0' +if args.geminittsstyle is not None: + myConfig.geminiTTSStyle = args.geminittsstyle +if args.streamingdefault: + myConfig.streamingDefault = args.streamingdefault != '0' if args.cmdwaittimeout: myConfig.cmdWaitTimeout = int(args.cmdwaittimeout) if args.pid: @@ -3301,13 +3737,18 @@ def shutdown(): if args.ttsweb: # Normalize base URL once for all paths (supports Jeedom in subdirectories) ttsweb_base_url = args.ttsweb.rstrip('/') - myConfig.ttsWebSrvCache = f'{ttsweb_base_url}/plugins/ttscast/data/cache/' - myConfig.ttsWebSrvMedia = f'{ttsweb_base_url}/plugins/ttscast/data/media/' + myConfig.ttsWebSrvCache = f'{ttsweb_base_url}/plugins/ttscast/core/php/ttscast.audio.proxy.php?type=tts&file=' + myConfig.ttsWebSrvMediaProxy = f'{ttsweb_base_url}/plugins/ttscast/core/php/ttscast.audio.proxy.php' myConfig.ttsWebSrvImages = f'{ttsweb_base_url}/plugins/ttscast/data/images/' myConfig.ttsWebSrvJeeTTS = f'{ttsweb_base_url}/core/api/' jeedom_utils.set_log_level(myConfig.logLevel) +if myConfig.castLogLevel != 'daemon': + _cast_level = jeedom_utils.convert_log_level(myConfig.castLogLevel) + logging.getLogger('pychromecast').setLevel(_cast_level) + logging.getLogger('zeroconf').setLevel(_cast_level) + if myConfig.cycleFactor == 0: myConfig.cycleMain = 2.0 myConfig.cycleComm = 0.5 @@ -3325,6 +3766,7 @@ def shutdown(): logging.info('[DAEMON][MAIN] Plugin Version: %s', myConfig.pluginVersion) logging.info('[DAEMON][MAIN] Python Version: %s', sys.version) logging.info('[DAEMON][MAIN] Log level: %s', myConfig.logLevel) +logging.info('[DAEMON][MAIN] Cast log level: %s', myConfig.castLogLevel) logging.info('[DAEMON][MAIN] Socket port: %s', myConfig.socketPort) logging.info('[DAEMON][MAIN] Socket host: %s', myConfig.socketHost) logging.info('[DAEMON][MAIN] CycleFactor: %s', myConfig.cycleFactor) @@ -3356,10 +3798,15 @@ def shutdown(): logging.info('[DAEMON][MAIN] AI Use Custom System Prompt: %s', str(myConfig.aiUseCustomSysPrompt)) logging.info('[DAEMON][MAIN] AI Custom System Prompt: %s', myConfig.aiCustomSysPrompt) logging.info('[DAEMON][MAIN] AI Default Tone: %s', myConfig.aiDefaultTone) +logging.info('[DAEMON][MAIN] Gemini TTS Enabled: %s', str(myConfig.geminiTTSEnabled)) +logging.info('[DAEMON][MAIN] Gemini TTS Model: %s', myConfig.geminiTTSModel) +logging.info('[DAEMON][MAIN] Gemini TTS Default: %s', str(myConfig.geminiTTSDefault)) +logging.info('[DAEMON][MAIN] Gemini TTS Style: %s', myConfig.geminiTTSStyle if myConfig.geminiTTSStyle else 'N/A') +logging.info('[DAEMON][MAIN] Gemini TTS Streaming Default: %s', str(myConfig.streamingDefault)) logging.info('[DAEMON][MAIN] Cmd Wait Timeout: %s', str(myConfig.cmdWaitTimeout)) logging.info('[DAEMON][MAIN] CallBack: %s', myConfig.callBack) logging.info('[DAEMON][MAIN] Jeedom WebSrvCache: %s', myConfig.ttsWebSrvCache) -logging.info('[DAEMON][MAIN] Jeedom WebSrvMedia: %s', myConfig.ttsWebSrvMedia) +logging.info('[DAEMON][MAIN] Jeedom WebSrvMediaProxy: %s', myConfig.ttsWebSrvMediaProxy) logging.info('[DAEMON][MAIN] Jeedom WebSrvImages: %s', myConfig.ttsWebSrvImages) logging.info('[DAEMON][MAIN] Jeedom WebSrvJeeTTS: %s', myConfig.ttsWebSrvJeeTTS) diff --git a/resources/ttscastd/utils.py b/resources/ttscastd/utils.py index 6332c4d5..64fd3996 100644 --- a/resources/ttscastd/utils.py +++ b/resources/ttscastd/utils.py @@ -51,6 +51,7 @@ class Config: ttsCacheFolder = 'data/cache' ttsCacheFolderWeb = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ttsCacheFolder)) ttsCacheFolderTmp = os.path.join('/tmp/jeedom/', 'ttscast_cache') + ttsStreamFolderTmp = os.path.join('/tmp/jeedom/ttscast_cache', 'stream') radiosFilePath = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'data/radios/radios.json')) customRadiosFilePath = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'data/radios/custom/radios.json')) @@ -58,7 +59,7 @@ class Config: # soundsCustomPath = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'data/media/custom')) ttsWebSrvCache = '' - ttsWebSrvMedia = '' + ttsWebSrvMediaProxy = '' ttsWebSrvImages = '' ttsWebSrvJeeTTS = '' @@ -73,7 +74,8 @@ class Config: # Clés d'options consommées par le démon (jamais renvoyées comme options de notification) DAEMON_OPTION_KEYS = frozenset({ 'genai', 'aitone', 'aisysprompt', 'aitemp', # IA - 'ssml', 'before', 'voice', # TTS voix + 'ssml', 'markup', 'style', 'before', 'voice', # TTS voix / format + 'engine', # TTS moteur (override par commande) 'volume', 'ding', 'wait', 'force', # TTS comportement }) @@ -92,7 +94,7 @@ def aiSysPrompt(self, aiCustomTone=None): 'Contenu :\n' '- Reformule la phrase d\'origine de façon naturelle et réponds à la question posée s\'il y en a une.\n' '- Conserve toutes les valeurs chiffrées présentes dans la phrase d\'origine.\n' - '- Si la phrase contient une notion temporelle (date, jour, heure, délai…), utilise la recherche en ligne pour vérifier la date et le jour de la semaine.\n' + '- Ne mentionne jamais la date ni le jour de la semaine de façon spontanée. Indique-les uniquement si la phrase d\'origine les demande explicitement (question sur la date, la météo du jour, un événement calendaire, un lever/coucher de soleil…).\n' '- Pour toute question nécessitant des données actuelles ou factuelles (météo, actualités, résultats…), utilise la recherche en ligne.\n\n' 'Format de tes réponses :\n' '- Réponds en phrases courtes et fluides, adaptées à une diffusion vocale.\n' @@ -103,6 +105,13 @@ def aiSysPrompt(self, aiCustomTone=None): aiUseCustomSysPrompt = False aiCustomSysPrompt = '' aiScopes = ['https://www.googleapis.com/auth/cloud-platform'] + + # Gemini TTS Configuration + geminiTTSEnabled = False + geminiTTSModel = 'noModel' + geminiTTSDefault = False + geminiTTSStyle = '' # Style par défaut, peut être surchargé par style: dans les options de scénario + streamingDefault = False # Gemini TTS uniquement — ignoré pour les autres moteurs # Paths for various resources mediaFolder = 'data/media' @@ -121,6 +130,7 @@ def aiSysPrompt(self, aiCustomTone=None): cmdWaitQueue = {} logLevel = "error" + castLogLevel = "daemon" socketPort = 55111 socketHost = 'localhost' pidFile = '/tmp/ttscastd.pid'