{"id":245,"date":"2024-12-07T06:15:05","date_gmt":"2024-12-07T06:15:05","guid":{"rendered":"https:\/\/textsnapper.com\/?page_id=245"},"modified":"2025-09-09T06:16:54","modified_gmt":"2025-09-09T06:16:54","slug":"tool","status":"publish","type":"page","link":"https:\/\/textsnapper.com\/en\/tool\/","title":{"rendered":"tool"},"content":{"rendered":"<div data-elementor-type=\"wp-page\" data-elementor-id=\"245\" class=\"elementor elementor-245\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-2bba8a3 e-flex e-con-boxed e-con e-parent\" data-id=\"2bba8a3\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-4689469 elementor-widget elementor-widget-heading\" data-id=\"4689469\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\"><a href=\"https:\/\/textsnapper.com\/en\/\">Have the picture read aloud<\/a><\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-e04154f elementor-widget elementor-widget-html\" data-id=\"e04154f\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<div id=\"translations\" style=\"display:none;\">\n    <span id=\"no-camera-de\">No camera available or permission denied.<\/span>\n    <span id=\"no-camera-en\">No camera available or permission denied.<\/span>\n\n    <span id=\"no-image-selected-de\">No image selected.<\/span>\n    <span id=\"no-image-selected-en\">No image selected.<\/span>\n\n    <span id=\"no-image-available-de\">No image available.<\/span>\n    <span id=\"no-image-available-en\">No image available.<\/span>\n\n    <span id=\"ocr-error-de\">Text recognition error. Please click again.<\/span>\n    <span id=\"ocr-error-en\">Error during text recognition. Please try again.<\/span>\n\n    <span id=\"no-text-recognized-de\">No text recognized.<\/span>\n    <span id=\"no-text-recognized-en\">No text recognized.<\/span>\n\n    <span id=\"select-voice-alert-de\">Please choose a voice!<\/span>\n    <span id=\"select-voice-alert-en\">Please select a voice!<\/span>\n\n    <span id=\"no-text-to-play-de\">No text to read aloud.<\/span>\n    <span id=\"no-text-to-play-en\">No text to play.<\/span>\n\n    <span id=\"no-text-to-share-de\">No text to share.<\/span>\n    <span id=\"no-text-to-share-en\">No text to share.<\/span>\n\n    <span id=\"share-not-supported-de\">Sharing is not supported here.<\/span>\n    <span id=\"share-not-supported-en\">Sharing is not supported here.<\/span>\n\n    <span id=\"nothing-to-copy-de\">Nothing to copy.<\/span>\n    <span id=\"nothing-to-copy-en\">Nothing to copy.<\/span>\n\n    <span id=\"text-copied-de\">Text copied successfully!<\/span>\n    <span id=\"text-copied-en\">Text successfully copied!<\/span>\n\n    <span id=\"no-text-to-save-de\">No text available to save.<\/span>\n    <span id=\"no-text-to-save-en\">No text available to save.<\/span>\n\n    <span id=\"text-filename-prompt-de\">Enter file name for the text document:<\/span>\n    <span id=\"text-filename-prompt-en\">Enter filename for the text document:<\/span>\n\n    <span id=\"text-save-error-de\">Error saving the text document.<\/span>\n    <span id=\"text-save-error-en\">Error saving text document.<\/span>\n\n    <span id=\"play-tts-first-de\">First play a text (TTS).<\/span>\n    <span id=\"play-tts-first-en\">Play text first (TTS).<\/span>\n\n    <span id=\"audio-filename-prompt-de\">Please specify the file name for the audio:<\/span>\n    <span id=\"audio-filename-prompt-en\">Please enter filename for audio:<\/span>\n\n    <span id=\"audio-save-error-de\">Error saving audio.<\/span>\n    <span id=\"audio-save-error-en\">Error saving audio.<\/span>\n    <span id=\"start-camera-de\">Start camera<\/span>\n  <span id=\"start-camera-en\">Start camera<\/span>\n\n  <span id=\"upload-image-de\">Upload image<\/span>\n  <span id=\"upload-image-en\">Upload image<\/span>\n\n  <span id=\"take-photo-de\">Foto aufnehmen<\/span>\n  <span id=\"take-photo-en\">Take photo<\/span>\n\n  <span id=\"process-text-de\">Text erkennen<\/span>\n  <span id=\"process-text-en\">Recognize text<\/span>\n\n  <span id=\"play-text-de\">Text vorlesen<\/span>\n  <span id=\"play-text-en\">Play text<\/span>\n\n  <span id=\"share-text-de\">Teilen<\/span>\n  <span id=\"share-text-en\">Share<\/span>\n\n  <span id=\"copy-text-de\">Kopieren<\/span>\n  <span id=\"copy-text-en\">Copy<\/span>\n\n  <span id=\"save-audio-de\">Audio speichern<\/span>\n  <span id=\"save-audio-en\">Save audio<\/span>\n\n  <span id=\"save-text-doc-de\">Text speichern<\/span>\n  <span id=\"save-text-doc-en\">Save text<\/span>\n\n  <span id=\"select-dialect-label-de\">Dialect:<\/span>\n  <span id=\"select-dialect-label-en\">Dialect:<\/span>\n\n  <span id=\"select-gender-label-de\">Gender:<\/span>\n  <span id=\"select-gender-label-en\">Gender:<\/span>\n\n  <span id=\"select-quality-label-de\">Quality \/ Type:<\/span>\n  <span id=\"select-quality-label-en\">Quality \/ Type:<\/span>\n\n  <span id=\"select-voice-label-de\">Voice:<\/span>\n  <span id=\"select-voice-label-en\">Voice:<\/span>\n\n  <span id=\"select-speed-label-de\">Speed:<\/span>\n  <span id=\"select-speed-label-en\">Speed:<\/span>\n\n  <span id=\"detected-language-de\">Recognized language: ...<\/span>\n  <span id=\"detected-language-en\">Detected language: ...<\/span>\n  <span id=\"recognized-text-label-de\">Recognized text:<\/span>\n<span id=\"recognized-text-label-en\">Recognized Text:<\/span>\n\n<span id=\"detected-language-prefix-de\">Erkannte Sprache:<\/span>\n<span id=\"detected-language-prefix-en\">Detected language:<\/span>\n\n<span id=\"voice-settings-hint-de\">Select an alternative voice directly from the dropdowns, or permanently save your preferred voice in the <a href=\"https:\/\/textsnapper.com\/en\/settings\/\" style=\"color:#2F5591; text-decoration:underline;\">language settings<\/a>.<\/span>\n\n<span id=\"voice-settings-hint-en\">Select an alternative voice directly via the dropdowns or permanently save your preferred voice in the <a href=\"https:\/\/textsnapper.com\/en\/settings\/\" style=\"color:#2F5591; text-decoration:underline;\">voice settings<\/a>.<\/span>\n<span id=\"detected-language-value-de\">German<\/span>\n<span id=\"detected-language-value-en\">German<\/span>\n\n<span id=\"any-de\">Beliebig<\/span>\n<span id=\"any-en\">Any<\/span>\n\n<span id=\"male-de\">Masculine<\/span>\n<span id=\"male-en\">Male<\/span>\n\n<span id=\"female-de\">Female<\/span>\n<span id=\"female-en\">Female<\/span>\n\n<span id=\"select-dialect-de\">Select dialect<\/span>\n<span id=\"select-dialect-en\">Select Dialect<\/span>\n\n<span id=\"select-gender-de\">Choose gender<\/span>\n<span id=\"select-gender-en\">Select Gender<\/span>\n\n<span id=\"select-quality-de\">Select quality<\/span>\n<span id=\"select-quality-en\">Select Quality<\/span>\n\n<span id=\"select-voice-de\">Select Voice<\/span>\n<span id=\"select-voice-en\">Select Voice<\/span>\n\n<span id=\"speed-slow-de\">Slow<\/span>\n<span id=\"speed-slow-en\">Slow<\/span>\n\n<span id=\"speed-normal-de\">Normal<\/span>\n<span id=\"speed-normal-en\">Normal<\/span>\n\n<span id=\"speed-fast-de\">Fast<\/span>\n<span id=\"speed-fast-en\">Fast<\/span>\n\n<span id=\"speed-very-fast-de\">Very fast<\/span>\n<span id=\"speed-very-fast-en\">Very fast<\/span>\n\n<span id=\"font-size-de\">Font size:<\/span>\n<span id=\"font-size-en\">Font size:<\/span>\n\n<span id=\"play-audio-de\">\ud83d\udd0a Read aloud<\/span>\n<span id=\"play-audio-en\">\ud83d\udd0a Play audio<\/span>\n\n<span id=\"loading-audio-de\">\u23f3 L\u00e4dt Audio...<\/span>\n<span id=\"loading-audio-en\">\u23f3 Loading audio...<\/span>\n\n<span id=\"back-button-fullscreen-de\">\u2190 Back<\/span>\n<span id=\"back-button-fullscreen-en\">\u2190 Back<\/span>\n\n<span id=\"play-audio-de\">\ud83d\udd0a Read aloud<\/span>\n<span id=\"play-audio-en\">\ud83d\udd0a Play text<\/span>\n\n<span id=\"stop-audio-de\">\u23f9\ufe0f Stopp<\/span>\n<span id=\"stop-audio-en\">\u23f9\ufe0f Stop<\/span>\n\n<span id=\"pause-audio-de\">\u23f8\ufe0f Pause<\/span>\n<span id=\"pause-audio-en\">\u23f8\ufe0f Pause<\/span>\n<\/div>\n\n<!-- OCR-TTS Widget -->\n<div id=\"ocr-tts-widget\" data-no-translation=\"\">\n  <!-- Controls-Top: Kamera und Datei-Upload -->\n  <div class=\"controls-top\">\n    <button id=\"startCamera\" class=\"btn\">\ud83d\udcf7  Kamera starten<\/button>\n    <button id=\"uploadImage\" class=\"btn\">\ud83d\udcc1  Bild hochladen<\/button>\n    <!-- Mit capture=\"environment\" wird auf mobilen Ger\u00e4ten direkt die Kamera-App ge\u00f6ffnet -->\n    <input type=\"file\" id=\"fileUpload\" accept=\"image\/*\" capture=\"environment\" style=\"display: none;\">\n<\/div>\n  <!-- Kamera-Container (anfangs ausgeblendet) -->\n  <div class=\"camera-container\" style=\"display: none;\">\n    <video id=\"camera\" autoplay playsinline style=\"display: none;\"><\/video>\n    <canvas id=\"snapshot\" style=\"display: none;\"><\/canvas>\n    <img decoding=\"async\" id=\"photoPreview\" src=\"\" alt=\"Vorschau\" style=\"display: none;\">\n  <\/div>\n  \n<!-- Fullscreen TTS Overlay -->\n<div id=\"fullscreenTTS\" class=\"fullscreen-overlay\" style=\"display:none;\">\n  <div class=\"fullscreen-header\">\n    <span id=\"closeFullscreen\"><\/span>\n<\/div>\n\n  <div class=\"fullscreen-content\">\n    <div id=\"fullscreenText\"><\/div>\n  <\/div>\n\n  <div class=\"fullscreen-footer\">\n  <button id=\"fullscreenPlay\" class=\"btn\">\ud83d\udd0a Vorlesen<\/button>\n<button id=\"fullscreenPause\" class=\"btn\" style=\"display:none;\">\u23f8\ufe0f Pause<\/button>\n<button id=\"fullscreenStop\" class=\"btn\" style=\"display:none;\">\u23f9\ufe0f Stopp<\/button>\n\n  <div class=\"font-controls\">\n    <label for=\"fontSizeSlider\">Schriftgr\u00f6\u00dfe:<\/label>\n    <button id=\"fontSmaller\" class=\"btn\">A-<\/button>\n    <input type=\"range\" id=\"fontSizeSlider\" min=\"16\" max=\"80\" value=\"32\">\n    <button id=\"fontLarger\" class=\"btn\">A+<\/button>\n  <\/div>\n<\/div>\n\n<\/div>\n\n<!-- ocr-result, Controls-Middle und Rotation-Buttons geh\u00f6ren hierhin (au\u00dferhalb von fullscreenTTS!) -->\n<div id=\"ocr-result\"><\/div>\n\n\n\n<!-- Controls-Middle: Foto aufnehmen und Text erkennen -->\n<div class=\"controls-middle\" style=\"display: none;\">\n  <button id=\"takePhoto\" class=\"btn\" style=\"display: none;\">\ud83d\udcf8 Foto aufnehmen<\/button>\n\n  <button id=\"processText\" class=\"btn\" style=\"display: none;\">\ud83d\udd0d Text erkennen<\/button>\n<\/div>\n\n\n  \n\n  <!-- Status-Anzeige (anfangs ausgeblendet) -->\n  <div class=\"status-container\" style=\"display: none;\">\n    <p id=\"languageStatus\">Erkannte Sprache: ...<\/p>\n    <p id=\"audioStatus\" style=\"display: none;\">\ud83c\udfb5 Audio wird abgespielt...<\/p>\n    <div id=\"loadingIndicator\" style=\"display: none;\">\u23f3 Verarbeite...<\/div>\n  <\/div>\n\n  <!-- Output-Container (anfangs ausgeblendet) -->\n  <div id=\"output\" style=\"display: none;\">\n    <!-- <p id=\"recognizedText\">Erkannter Text erscheint hier...<\/p> -->\n    \n<div id=\"recognizedTextContainer\" style=\"display:none; margin-top:20px; padding:15px; border:1px solid #28a745; border-radius:8px; background:#e9f9ee;\">\n  <h3 style=\"color:#28a745; margin-bottom:10px;\">Erkannter Text:<\/h3>\n  <div id=\"recognizedTextContent\" style=\"font-size:16px; line-height:1.6;\"><\/div>\n<\/div>\n\n    <div id=\"playWrapper\" style=\"text-align: center; margin-top: 20px;\">\n  <button id=\"playText\" class=\"btn play-btn\">\ud83d\udd0a Text vorlesen<\/button>\n<\/div>\n\n<div id=\"actionButtons\" class=\"button-group-split\">\n  <div class=\"button-col left\">\n    <button id=\"shareText\" class=\"btn alt-btn\">\ud83d\udce4 Teilen<\/button>\n    <button id=\"copyText\" class=\"btn alt-btn\">\ud83d\udccb Kopieren<\/button>\n  <\/div>\n  <div class=\"button-col right\">\n    <button id=\"saveAudio\" class=\"btn alt-btn\">\ud83d\udcbe Audio in Mein Archiv speichern<\/button>\n    <button id=\"saveTextDoc\" class=\"btn alt-btn\">\ud83d\udcbe Text in Mein Archiv speichern<\/button>\n  <\/div>\n<\/div>\n\n\n    <!-- NEU: Hinweis zum Speichern unterhalb vom Button -->\n    <p id=\"audioSaveNotice\" style=\"display: none; margin-top: 10px; color: green;\"><\/p>\n  <\/div>\n\n<div id=\"voiceSettingsHint\" style=\"display:none; margin-top:15px; margin-bottom:15px; padding:10px; background-color:#fff8e6; border:1px solid #ffd580; border-radius:6px; text-align:center;\">\n  W\u00e4hle \u00fcber die Dropdowns direkt eine alternative Stimme oder speichere deine bevorzugte Stimme dauerhaft in den \n  <a href=\"https:\/\/textsnapper.com\/settings\/\" style=\"color:#2F5591; text-decoration:underline;\">Spracheinstellungen<\/a>.\n<\/div>\n\n  <div class=\"selection-container\" style=\"display: none;\">\n    <!-- Dialekt -->\n    <div class=\"dialect-selection\" style=\"display: none;\">\n      <label for=\"dialectSelect\">Dialekt:<\/label>\n      <select id=\"dialectSelect\" class=\"dropdown\">\n        <option value=\"\" disabled selected>Dialekt ausw\u00e4hlen<\/option>\n      <\/select>\n    <\/div>\n\n    <!-- Geschlecht -->\n<div class=\"gender-selection\" style=\"display: none;\">\n  <label for=\"genderSelect\">Geschlecht:<\/label>\n  <select id=\"genderSelect\" class=\"dropdown\">\n    <option value=\"\" disabled selected data-translation-key=\"select-gender\">Geschlecht w\u00e4hlen<\/option>\n    <option value=\"M\u00c4NNLICH\" data-translation-key=\"male\">M\u00e4nnlich<\/option>\n    <option value=\"WEIBLICH\" data-translation-key=\"female\">Weiblich<\/option>\n  <\/select>\n<\/div>\n\n<!-- Qualit\u00e4t -->\n<div class=\"quality-selection\" style=\"display: none;\">\n  <label for=\"qualitySelect\">Qualit\u00e4t \/ Typ:<\/label>\n  <select id=\"qualitySelect\" class=\"dropdown\">\n    <option value=\"\" disabled selected data-translation-key=\"select-quality\">Qualit\u00e4t ausw\u00e4hlen<\/option>\n  <\/select>\n<\/div>\n\n<!-- Stimme -->\n<div class=\"voice-selection\">\n  <label for=\"voiceSelect\">Stimme:<\/label>\n  <select id=\"voiceSelect\" class=\"dropdown\">\n    <option value=\"\" disabled selected data-translation-key=\"select-voice\">Stimme ausw\u00e4hlen<\/option>\n  <\/select>\n<\/div>\n\n\n    <!-- Sprechgeschwindigkeit -->\n    <div class=\"speed-selection\">\n  <label for=\"speedSelectValue\" id=\"speedLabel\">Geschwindigkeit:<\/label>\n  <button id=\"speedDecrease\" class=\"btn\" type=\"button\" style=\"min-width:40px;\">-<\/button>\n  <input id=\"speedSelectValue\" type=\"text\" value=\"1.00\" readonly style=\"width:55px;text-align:center;font-size:1.1em; border:1px solid #ccc; border-radius:4px; margin:0 8px;\">\n  <button id=\"speedIncrease\" class=\"btn\" type=\"button\" style=\"min-width:40px;\">+<\/button>\n  <!-- Hidden: bleibt f\u00fcr JS-Kompatibilit\u00e4t -->\n  <input id=\"speedSelect\" type=\"hidden\" value=\"1\">\n<\/div>\n\n  <\/div>\n<\/div>\n\n<!-- Gespeicherte Stimmen-Liste -->\n<div id=\"savedVoicesOuterContainer\" style=\"display:none; border:1px solid #ccc; padding:15px; max-width:700px; margin:10px auto; border-radius:6px; background:#F9F9F9; box-shadow:0 2px 8px rgba(0,0,0,0.1);\">\n    <div id=\"userSavedVoicesContainer\" style=\"margin-top:0; text-align:center;\">\n      <h3 style=\"cursor:pointer; user-select:none;\" onclick=\"toggleSavedVoices()\">\n        My saved votes (<span id=\"toggleArrow\">\u25bcexpand\u25bc<\/span>)\n      <\/h3>\n      <div id=\"userSavedVoicesList\" style=\"max-width:400px; margin:0 auto; display:none;\"><\/div>\n    <\/div>\n  <\/div>\n\n<!-- Eingebettetes CSS -->\n<style>\n  #ocr-tts-widget {\n    border: 1px solid #ccc;\n    padding: 20px;\n    max-width: 700px;\n    margin: 20px auto;\n    font-family: Arial, sans-serif;\n    background-color: #f9f9f9;\n    border-radius: 8px;\n    box-shadow: 0 2px 8px rgba(0,0,0,0.1);\n  }\n  .controls-top,\n  .controls-middle,\n  .selection-container,\n  .status-container,\n  #output {\n    margin-bottom: 15px;\n    text-align: center;\n  }\n  #ocr-tts-widget .btn {\n    padding: 10px 15px;\n    margin: 5px;\n    background-color: #2F5591;\n    color: #fff;\n    border: none;\n    cursor: pointer;\n    border-radius: 5px;\n    transition: background-color 0.3s ease;\n  }\n  #ocr-tts-widget .btn:hover {\n    background-color: #1d3f6c;\n  }\n  #ocr-tts-widget .dropdown {\n    padding: 8px;\n    width: 100%;\n    max-width: 220px;\n    border: 1px solid #ccc;\n    border-radius: 4px;\n    margin-top: 5px;\n  }\n  #ocr-tts-widget .camera-container,\n  .status-container {\n    text-align: center;\n  }\n  #camera {\n    width: 100%;\n    max-width: 100%;\n    height: auto;\n    border-radius: 8px;\n  }\n  #snapshot,\n  #photoPreview {\n    width: 100%;\n    max-width: 100%;\n    height: auto;\n    border: 1px solid #ccc;\n    border-radius: 8px;\n    margin-top: 10px;\n  }\n  .selection-container label {\n    display: block;\n    margin-bottom: 5px;\n    font-weight: bold;\n  }\n  .status-container p {\n    margin: 5px 0;\n    font-size: 14px;\n  }\n  #loadingIndicator {\n    font-style: italic;\n    color: #555;\n  }\n  .gender-selection,\n  .quality-selection {\n    margin-top: 10px;\n  }\n  \/* Gespeicherte Stimmen - Layout verbessern *\/\n  #userSavedVoicesList .saved-voice-item {\n    display: flex;\n    align-items: center;\n    margin-bottom: 8px;\n    background: #fff;\n    padding: 8px;\n    border: 1px solid #ccc;\n    border-radius: 6px;\n  }\n  #userSavedVoicesList .saved-voice-item img {\n    width: 50px; \n    height: 50px; \n    object-fit: cover; \n    margin-right: 10px; \n    border: 1px solid #ccc;\n    border-radius: 4px;\n  }\n  #userSavedVoicesList .saved-voice-item span {\n    font-size: 14px;\n    line-height: 1.4;\n  }\n  #userSavedVoicesContainer h3 {\n    color: #2F5591;\n  }\n#processText {\n  display: none !important;\n  visibility: hidden;\n}\n   .ocr-image-container {\n  position: relative;\n  display: inline-block; \/* beibehalten *\/\n  width: 100%;           \/* Bild nutzt gesamte Breite *\/\n  max-width: 100%;\n}\n.ocr-image-container img {\n  width: 100%;           \/* Bild auf 100% Breite *\/\n  height: auto;          \/* Proportionale H\u00f6he *\/\n  border-radius: 8px;    \/* \u00c4hnlich wie photoPreview *\/\n  border: 1px solid #ccc; \/* Gleicher Stil wie zuvor *\/\n}\n  .ocr-box {\n    position: absolute;\n    border: 3px solid rgba(255,0,0,0.6);\n    background-color: rgba(255, 255, 0, 0.4);\n    cursor: pointer;\n  }\n#ocr-result {\n  position: relative;\n  width: 100%;\n  max-width: 100%;\n}\n.ocr-image-container {\n  position: relative;\n  display: inline-block;\n  width: 100%;\n  max-width: 100%;\n  margin: 0 auto; \/* sicherstellen *\/\n  padding: 0;\n  box-sizing: border-box;\n}\n    .modal-overlay {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background-color: rgba(0, 0, 0, 0.8);\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    z-index: 1000;\n  }\n\n  .modal-content {\n    background-color: #fff;\n    padding: 15px;\n    border-radius: 8px;\n    max-width: 80%;\n    max-height: 80%;\n    overflow-y: auto;\n    text-align: center;\n  }\n\n  .modal-content img {\n    max-width: 100%;\n    height: auto;\n    border-radius: 5px;\n  }\n\n  .text-block {\n    background: #eee;\n    margin: 10px;\n    padding: 10px;\n    border-radius: 4px;\n    cursor: pointer;\n    transition: background-color 0.3s;\n  }\n\n  .text-block:hover {\n    background-color: #ddd;\n  }\n\n  .large-text {\n    font-size: 1.5em;\n    padding: 20px;\n  }\n\n  .close-modal {\n    margin-top: 10px;\n    cursor: pointer;\n    color: #fff;\n    background-color: #2F5591;\n    border: none;\n    padding: 5px 10px;\n    border-radius: 4px;\n  }\n\n.fullscreen-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background-color: #ffffff;\n  z-index: 10000;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden; \/* Gesamt-Scrolling verhindern *\/\n}\n\n.fullscreen-header {\n  flex: 0 0 auto;\n  padding: 15px;\n  font-size: 20px;\n  font-weight: bold;\n  cursor: pointer;\n  border-bottom: 1px solid #ddd;\n  background-color: #ffffff;\n  position: sticky; \/* Fixiert beim Scrollen *\/\n  top: 0;\n  z-index: 10002; \/* wichtig, oberhalb des Inhalts *\/\n}\n\n.fullscreen-content {\n  flex: 1 1 auto;\n  padding: 20px;\n  overflow-y: auto; \/* Nur hier scrolling! *\/\n  font-size: 1.3em;\n  line-height: 1.6;\n  color: #333;\n  position: relative;\n}\n\n.fullscreen-footer {\n  flex: 0 0 auto;\n  padding: 15px;\n  border-top: 1px solid #ddd;\n  text-align: center;\n  background-color: #ffffff;\n  position: sticky; \/* Fixiert beim Scrollen *\/\n  bottom: 0;\n  z-index: 10002; \/* wichtig, oberhalb des Inhalts *\/\n  box-shadow: 0 -2px 5px rgba(0,0,0,0.1);\n}\n\n.fullscreen-footer .btn {\n  padding: 15px 25px;\n  font-size: 1.1em;\n}\n\/* Buttons global stylen *\/\n.btn {\n  padding: 10px 15px;\n  margin: 5px;\n  background-color: #2F5591;\n  color: #fff;\n  border: none;\n  cursor: pointer;\n  border-radius: 5px;\n  transition: background-color 0.3s ease;\n}\n\n\/* Hover-Effekt Buttons global *\/\n.btn:hover {\n  background-color: #1d3f6c;\n}\n\n\/* Dropdown global stylen *\/\n.dropdown {\n  padding: 8px;\n  width: 100%;\n  max-width: 220px;\n  border: 1px solid #ccc;\n  border-radius: 4px;\n  margin-top: 5px;\n}\n.highlight {\n  background-color: yellow;\n}\n#recognizedTextContainer {\n  display: none;\n  margin-top: 20px;\n  padding: 15px;\n  border: 1px solid #28a745;\n  border-radius: 8px;\n  background: #e9f9ee;\n}\n\n#recognizedTextContainer h3 {\n  color: #28a745;\n  margin-bottom: 10px;\n}\n\n#recognizedTextContent {\n  font-size: 16px;\n  line-height: 1.6;\n}\n#closeFullscreen {\n  display: inline-block;\n  background-color: #2F5591;\n  color: #fff;\n  padding: 10px 15px;\n  border-radius: 5px;\n  cursor: pointer;\n  font-size: 18px;\n  transition: background-color 0.3s ease;\n}\n\n#closeFullscreen:hover {\n  background-color: #1d3f6c;\n}\n\/* Einzelner Play-Button *\/\n#playWrapper .play-btn {\n  background-color: #2F5591;\n  font-size: 20px;\n  padding: 15px 30px;\n}\n\n\/* Gruppe f\u00fcr andere Buttons *\/\n.button-group {\n  margin-top: 20px;\n  text-align: center;\n}\n\n\/* Alternative Button-Stil *\/\n.btn.alt-btn {\n  background-color: #333333 !important;\n  color: #fff;\n}\n\n.btn.alt-btn:hover {\n  background-color: #435943 !important;\n}\n\n.button-group-split {\n  display: flex;\n  justify-content: space-between;\n  gap: 32px;\n  margin-top: 20px;\n  margin-bottom: 15px;\n}\n\n.button-group-split .button-col {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  flex: 1 1 0;\n}\n\n.button-group-split .left,\n.button-group-split .right {\n  width: 50%;\n}\n\n.button-group-split .btn {\n  width: 100%;\n  min-width: 0;\n  box-sizing: border-box;\n  font-size: 1.1em;\n  text-align: left;\n  margin-bottom: 0;\n}\n\n\/* Rotation-Buttons responsive zentrieren und neu f\u00e4rben (nur Mobile) *\/\n@media (max-width: 768px) {\n \n.font-controls {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 10px;\n  margin-top: 15px;\n}\n\n.font-controls label {\n  font-size: 20px;\n  font-weight: bold;\n}\n\n.font-controls .btn {\n  padding: 15px 20px;\n  font-size: 20px;\n}\n\n#fontSizeSlider {\n  width: 50%;\n  height: 15px;\n}\n\n  .button-group-split {\n    flex-direction: column;\n    gap: 10px;\n  }\n  .button-group-split .button-col {\n    flex-direction: column;\n    width: 100%;\n    gap: 10px;\n  }\n  .button-group-split .btn {\n    width: 100%;\n  }\n}\n@media (min-width: 768px) and (max-width: 1024px) {\n  #ocr-tts-widget .btn {\n    padding: 25px 35px;     \n    font-size: 28px;        \n    margin: 25px;             \n  }\n\n  #ocr-tts-widget .dropdown {\n    padding: 14px;\n    font-size: 18px;\n  }\n\n  \/* \u00dcberschrift \u201eMeine gespeicherten Stimmen\u201c *\/\n  #userSavedVoicesContainer h3 {\n    font-size: 28px;      \n  }\n  .fullscreen-content {\n    font-size: 2em; \/* Gr\u00f6\u00dfere Grundschrift *\/\n  }\n\n  .font-controls label,\n  .font-controls .btn {\n    font-size: 24px;\n    padding: 18px 25px;\n  }\n\n  #fontSizeSlider {\n    height: 20px;\n  }\n\n}\n  \n<\/style>\n\n<script>\n  const userVoiceSettings = window.userVoiceSettings || {};\n  const ajaxurl = \"https:\/\/textsnapper.com\/wp-admin\/admin-ajax.php\";\n\n\n  let availableVoices = {};\n  let googleVoicesList = [];\n  let voiceQuality = \"standard\"; \/\/ Standardwert festlegen\n\n\n  const languageDisplayMap = {\n    \"en\": \"Englisch\",\n    \"de\": \"Deutsch\",\n    \"fr\": \"Franz\u00f6sisch\",\n    \"es\": \"Spanisch\",\n    \"it\": \"Italienisch\",\n    \"ar\": \"Arabisch\",\n    \"pt\": \"Portugiesisch\",\n    \"ru\": \"Russisch\",\n  };\n\n  const startCameraButton = document.getElementById(\"startCamera\");\n  const uploadImageButton = document.getElementById(\"uploadImage\");\n  const fileUpload        = document.getElementById(\"fileUpload\");\n  const camera            = document.getElementById(\"camera\");\n  const snapshot          = document.getElementById(\"snapshot\");\n  const photoPreview      = document.getElementById(\"photoPreview\");\n  const takePhotoButton   = document.getElementById(\"takePhoto\");\n  const processTextButton = document.getElementById(\"processText\");\n  const loadingIndicator  = document.getElementById(\"loadingIndicator\");\n  const recognizedTextEl  = document.getElementById(\"recognizedText\");\n  const playTextButton    = document.getElementById(\"playText\");\n  const shareTextButton   = document.getElementById(\"shareText\");\n  const copyTextButton    = document.getElementById(\"copyText\");\n  const saveAudioButton   = document.getElementById(\"saveAudio\");\n  const saveTextDocButton = document.getElementById(\"saveTextDoc\"); \/\/ Neu\n  const speedSelect       = document.getElementById(\"speedSelect\");\n  const audioStatus       = document.getElementById(\"audioStatus\");\n  const output            = document.getElementById(\"output\");\n  const voiceSelect       = document.getElementById(\"voiceSelect\");\n  const languageStatus    = document.getElementById(\"languageStatus\");\n\n  const dialectSelect     = document.getElementById(\"dialectSelect\");\n  const genderSelect      = document.getElementById(\"genderSelect\");\n  const qualitySelect     = document.getElementById(\"qualitySelect\");\n\n  const cameraContainer     = document.querySelector(\".camera-container\");\n  const controlsMiddle      = document.querySelector(\".controls-middle\");\n  const selectionContainer  = document.querySelector(\".selection-container\");\n  const statusContainer     = document.querySelector(\".status-container\");\n\n  const savedVoicesContainer = document.getElementById(\"userSavedVoicesContainer\");\n  const savedVoicesList      = document.getElementById(\"userSavedVoicesList\");\n\n  \/\/ Element f\u00fcr Erfolgsmeldungen\n  const audioSaveNotice   = document.getElementById(\"audioSaveNotice\");\n  \n\n  let capturedImage   = null;\n  let recognizedText  = \"\";\n  let mainLangCode    = \"\";\n  let selectedDialect = \"\";\n  let selectedGender  = \"\";\n  let selectedQuality = \"\";\n  let ttsAudio        = null;\n  let currentChunkIndex = 0;\n  let chunks = [];\n  let ttsAudioObj = null;\n  let ttsPlaying = false;\n  let isPaused = false;\n  \n  const fontSizeSlider = document.getElementById('fontSizeSlider');\nconst fullscreenText = document.getElementById('fullscreenText');\nconst fullscreenPause = document.getElementById('fullscreenPause');\nconst fullscreenStop = document.getElementById('fullscreenStop');\n\nconst fontLarger = document.getElementById('fontLarger');\nconst fontSmaller = document.getElementById('fontSmaller');\n\n\/\/ Slider-Steuerung\nfontSizeSlider.addEventListener('input', () => {\n  fullscreenText.style.fontSize = `${fontSizeSlider.value}px`;\n});\n\n\/\/ Schriftgr\u00f6\u00dfe vergr\u00f6\u00dfern\/verkleinern per Button\nfontLarger.addEventListener('click', () => {\n  let currentSize = parseInt(fontSizeSlider.value);\n  if (currentSize < 80) { \/\/ Maximalwert\n    currentSize += 4;\n    fontSizeSlider.value = currentSize;\n    fullscreenText.style.fontSize = `${currentSize}px`;\n  }\n});\n\nfontSmaller.addEventListener('click', () => {\n  let currentSize = parseInt(fontSizeSlider.value);\n  if (currentSize > 16) { \/\/ Minimalwert\n    currentSize -= 4;\n    fontSizeSlider.value = currentSize;\n    fullscreenText.style.fontSize = `${currentSize}px`;\n  }\n});\n\n\/\/ Setze initial gr\u00f6\u00dfere Schriftgr\u00f6\u00dfe beim \u00d6ffnen von Fullscreen\ndocument.getElementById('fullscreenTTS').addEventListener('show', () => {\n  fontSizeSlider.value = 32;  \/\/ Default-Wert beim \u00d6ffnen\n  fullscreenText.style.fontSize = '32px';\n});\n\nfunction getTranslation(id) {\n  const lang = document.documentElement.lang.startsWith('en') ? 'en' : 'de';\n  const el = document.getElementById(`${id}-${lang}`);\n  return el ? el.textContent : '';\n}\n\nfunction translateVoiceOptions() {\n  const lang = document.documentElement.lang.startsWith('en') ? 'en' : 'de';\n\n  const languageNames = new Intl.DisplayNames([lang], { type: 'language' });\n  const regionNames = new Intl.DisplayNames([lang], { type: 'region' });\n\n  dialectSelect.querySelectorAll('option').forEach(option => {\n    if (!option.value) return;\n    const nativeName = option.textContent;\n    const langCode = option.value.split('-')[0];\n    const regionCode = option.value.split('-')[1];\n    const translatedLang = languageNames.of(langCode) || nativeName;\n    const translatedRegion = regionNames.of(regionCode) || regionCode;\n    option.textContent = `${translatedLang} (${translatedRegion})`;\n  });\n\n  genderSelect.querySelectorAll('option').forEach(option => {\n    const key = option.getAttribute('data-translation-key');\n    const translationEl = document.getElementById(`${key}-${lang}`);\n    if (translationEl) option.textContent = translationEl.textContent;\n  });\n\n  qualitySelect.querySelectorAll('option').forEach(option => {\n    option.textContent = option.textContent.replace(\/Standard\/i, lang === 'en' ? 'Standard' : 'Standard');\n  });\n\n  voiceSelect.querySelectorAll('option').forEach(option => {\n    option.textContent = option.textContent\n      .replace(\/Weiblich|Female\/i, lang === 'en' ? 'Female' : 'Weiblich')\n      .replace(\/M\u00e4nnlich|Male\/i, lang === 'en' ? 'Male' : 'M\u00e4nnlich')\n      .replace(\/Standard\/i, lang === 'en' ? 'Standard' : 'Standard');\n  });\n\n  const speedMap = { \"Langsam\": \"Slow\", \"Normal\": \"Normal\", \"Schnell\": \"Fast\", \"Sehr schnell\": \"Very fast\" };\n  speedSelect.querySelectorAll('option').forEach(option => {\n    option.textContent = lang === 'en' \n      ? speedMap[option.textContent] || option.textContent \n      : Object.keys(speedMap).find(key => speedMap[key] === option.textContent) || option.textContent;\n  });\n\n  const detectedLang = languageStatus.textContent.split(': ')[1];\n  const detectedLangCode = Object.entries(languageDisplayMap).find(([_, val]) => val === detectedLang)?.[0] || detectedLang;\n  languageStatus.textContent = `${getTranslation(\"detected-language-prefix\")} ${languageNames.of(detectedLangCode)}`;\n}\n\n\nasync function fetchUserMembershipLevel() {\n  let fd = new URLSearchParams();\n  fd.set(\"action\", \"get_user_membership\");\n  try {\n    let response = await fetch(window.ajaxurl, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\n      body: fd,\n      credentials: \"include\"\n    });\n    let result = await response.json();\n\n    if (result.success && result.data.membership_name) {\n      const membershipName = result.data.membership_name.toLowerCase();\n\n      if (membershipName.includes('premium')) {\n        return 'premium';\n      } else if (membershipName.includes('diamond')) {\n        return 'diamond';\n      } else {\n        return 'basic';\n      }\n\n    } else {\n      return 'basic';\n    }\n  } catch (e) {\n    console.error(\"Fehler beim Abfragen des Membership-Levels:\", e);\n    return 'basic';\n  }\n}\n\nasync function updateVoiceDropdownAccessibility() {\n  voiceSelect.querySelectorAll(\"option\").forEach(option => {\n    option.disabled = false;\n    option.textContent = option.textContent.replace(\" (Premium)\", \"\");\n  });\n}\n\n\n\/\/ Hilfsfunktion um Voice-Quality zu ermitteln\nfunction getVoiceQuality(optionValue) {\n  try {\n    const { dial, voiceId } = JSON.parse(optionValue);\n    const voices = availableVoices[dial]?.dialects[dial] || [];\n    const foundVoice = voices.find(v => v.voiceId === voiceId);\n    return foundVoice ? foundVoice.quality.toLowerCase() : \"standard\";\n  } catch (e) {\n    return \"standard\";\n  }\n}\n\n\n\n  \/* 1) Verschachtelte (alte) Google-Stimmen abrufen *\/\n  async function fetchAvailableVoices() {\n    try {\n      const resp = await fetch(ajaxurl, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\n        body: new URLSearchParams({ action: \"get_available_voices\" })\n      });\n      if (!resp.ok) throw new Error(\"HTTP error\");\n      const data = await resp.json();\n      if (!data.success) throw new Error(data.data || \"Fehler beim Abrufen der Stimmen\");\n      availableVoices = data.data;\n    } catch (e) {\n      console.warn(\"Fehler in fetchAvailableVoices:\", e);\n    }\n  }\n\n  \/* 1b) Flaches JSON google_stimmen.json laden *\/\n  async function fetchGoogleVoices() {\n    try {\n      const resp = await fetch(\"https:\/\/textsnapper.com\/wp-content\/textsnapper\/google_stimmen.json\");\n      if (!resp.ok) throw new Error(\"HTTP error\");\n      googleVoicesList = await resp.json();\n    } catch(e) {\n      console.warn(\"Fehler in fetchGoogleVoices:\", e);\n    }\n  }\n\n  \/* 2) Gespeicherte User-Stimmen laden *\/\n  async function fetchUserSavedVoices() {\n    const formData = new URLSearchParams({ action: \"my_get_user_voices\" });\n    try {\n      const response = await fetch(ajaxurl, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\n        body: formData\n      });\n      const json = await response.json();\n      if (json.success && json.data && json.data.voices) {\n        const userVoices = json.data.voices;\n        Object.keys(userVoices).forEach(dial => {\n          userVoiceSettings[dial] = userVoices[dial];\n        });\n        updateUserSavedVoicesUI(userVoices);\n      }\n    } catch (e) {\n      console.warn(\"Fehler in fetchUserSavedVoices:\", e);\n    }\n  }\n\n  \/* Hilfsfunktion: passendes Objekt in google_stimmen.json finden *\/\n  function findVoiceDataById(voiceId) {\n    if (!voiceId || !Array.isArray(googleVoicesList)) return null;\n    return googleVoicesList.find(item => item.code2 === voiceId) || null;\n  }\n\n  \/\/ OCR-Bild anzeigen und Textfelder erstellen\n  function displayImageAndTextBlocks(imageSrc, ocrText) {\n    const paragraphs = ocrText.split(\/\\n+\/);\n    let textHtml = paragraphs.map(p => `<div class=\"text-block\">${p}<\/div>`).join('');\n\n    const content = `<img decoding=\"async\" src=\"${imageSrc}\" alt=\"OCR-Bild\">${textHtml}`;\n    const modal = showModal(content);\n\n    modal.querySelectorAll('.text-block').forEach(block => {\n      block.addEventListener('click', () => {\n        const largeText = `<div class=\"large-text\">${block.textContent}<\/div><button id=\"tts-play\" class=\"close-modal\">\ud83d\udd0a Vorlesen<\/button>`;\n        const textModal = showModal(largeText);\n\n        textModal.querySelector('#tts-play').addEventListener('click', () => playTTS(block.textContent));\n      });\n    });\n  }\n\n  \/* Gespeicherte Stimmen-Liste: Design in einer Zeile *\/\n  function updateUserSavedVoicesUI(voicesObj) {\n    const dialCodes = Object.keys(voicesObj);\n    if (dialCodes.length === 0) {\n      savedVoicesContainer.style.display = \"none\";\n      savedVoicesList.innerHTML = \"\";\n      return;\n    }\n    savedVoicesContainer.style.display = \"block\";\n    let html = \"\";\n    dialCodes.forEach(dc => {\n      const { voiceId, gender, quality } = voicesObj[dc] || {};\n      const finalId = voiceId || dc;\n\n      const fullData = findVoiceDataById(finalId);\n      let bildUrl = \"\";\n      let dialektName = dc;\n      if (fullData) {\n        bildUrl     = fullData.Bild;\n        dialektName = fullData.Dialekt_Name;\n      }\n\n      html += `\n        <div class=\"saved-voice-item\">\n          ${bildUrl ? `<img decoding=\"async\" src=\"${bildUrl}\" alt=\"${finalId}\" \/>` : \"\"}\n          <span>\n            <strong>${dialektName}<\/strong>\n            <br>Stimme-ID: ${finalId}<br>\n            Geschlecht: ${gender || \"\"}, Qualit\u00e4t: ${quality || \"\"}\n          <\/span>\n        <\/div>\n      `;\n    });\n    savedVoicesList.innerHTML = html;\n  }\n\n  \/* Kamera \/ Upload *\/\n  startCameraButton.addEventListener(\"click\", async () => {\n  \/\/ F\u00fcr mobile Ger\u00e4te: Kamera \u00f6ffnen\n  if (\/Mobi|Android\/i.test(navigator.userAgent)) {\n    fileUpload.setAttribute(\"capture\", \"environment\");\n    fileUpload.click();\n    return;\n  }\n  try {\n    const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: \"environment\" }, audio: false });\n    camera.srcObject = stream;\n    cameraContainer.style.display = \"block\";\n    camera.style.display = \"block\";\n    controlsMiddle.style.display = \"block\";\n    takePhotoButton.style.display = \"inline-block\";\n  } catch (err) {\n    alert(getTranslation(\"no-camera\"));\n  }\n  document.getElementById('savedVoicesOuterContainer').style.display = 'block';\n});\n\nuploadImageButton.addEventListener(\"click\", () => {\n  \/\/ Capture-Attribut entfernen, damit die Galerie (statt der Kamera) ge\u00f6ffnet wird\n  fileUpload.removeAttribute(\"capture\");\n  fileUpload.click();\n  document.getElementById('savedVoicesOuterContainer').style.display = 'block';\n});\n\n\nfileUpload.addEventListener(\"change\", e => {\n  const file = e.target.files[0];\n  if (!file) return alert(getTranslation(\"no-image-selected\"));\n\n  const reader = new FileReader();\nreader.onload = event => {\n  renderImageToCanvas(event.target.result, (finalBase64) => {\n    capturedImage = finalBase64;\n    photoPreview.src = capturedImage;\n    \n   photoPreview.style.display = \"block\";\nprocessTextButton.style.display = \"inline-block\";\nsetTimeout(() => processTextButton.click(), 300); \/\/ Warte kurz, damit das Bild sichtbar wird\n\n    cameraContainer.style.display = \"block\";\n    controlsMiddle.style.display = \"block\";\n    camera.style.display = \"none\";\n    takePhotoButton.style.display = \"none\";\n  });\n};\nreader.readAsDataURL(file);\n\n});\n\n\n\n\n  takePhotoButton.addEventListener(\"click\", () => {\n    const ctx = snapshot.getContext(\"2d\");\n    snapshot.width = camera.videoWidth;\n    snapshot.height = camera.videoHeight;\n    ctx.drawImage(camera, 0, 0, snapshot.width, snapshot.height);\n    capturedImage = snapshot.toDataURL(\"image\/png\");\n    photoPreview.src = capturedImage;\n    photoPreview.style.display = \"block\";\nprocessTextButton.style.display = \"inline-block\";\nsetTimeout(() => processTextButton.click(), 300);\n\n    camera.style.display = \"none\";\n    takePhotoButton.style.display = \"none\";\n  });\n\n\/\/ Splitte Text in Chunks zu ca. 200 W\u00f6rtern und maximal 4950 Zeichen, am Satzende\nfunction splitTextIntoChunks(text, maxWords = 200, maxChars = 4950) {\n    const sentences = text.match(\/[^.!?]+[.!?]+|\\n+|.+$\/g) || [];\n    let chunks = [];\n    let current = '';\n    let wordCount = 0;\n\n    for (let sentence of sentences) {\n        let wordsInSentence = sentence.trim().split(\/\\s+\/).length;\n        \/\/ Pr\u00fcfen, ob der aktuelle Chunk mit diesem Satz zu gro\u00df w\u00e4re (Wortzahl oder Zeichen)\n        if (\n            (wordCount + wordsInSentence > maxWords || (current + sentence).length > maxChars) &&\n            current.length > 0\n        ) {\n            chunks.push(current.trim());\n            current = '';\n            wordCount = 0;\n        }\n        current += sentence;\n        wordCount += wordsInSentence;\n    }\n    if (current.length > 0) chunks.push(current.trim());\n    return chunks;\n}\n\n\nprocessTextButton.addEventListener(\"click\", async () => {\n  if (!capturedImage) return alert(getTranslation(\"no-image-available\"));\n\n  loadingIndicator.style.display = \"block\";\n  statusContainer.style.display = \"block\";\n  languageStatus.textContent = \"Erkannte Sprache: ...\";\n  languageStatus.style.color = \"#2F5591\";\n\n  try {\n    const resp = await fetch(ajaxurl, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\n      body: new URLSearchParams({\n        action: \"process_ocr\",\n        image_data: capturedImage\n      })\n    });\n\n    if (!resp.ok) throw new Error(\"HTTP error\");\n    const j = await resp.json();\n    if (!j.success) throw new Error(j.data || \"Fehler bei OCR\");\n\n    const rotationDegrees = detectCorrectRotation(j.data.boxes);\n\n    rotateImageWithBoxes(capturedImage, j.data.boxes, rotationDegrees, async ({ rotatedImage, rotatedBoxes }) => {\n      capturedImage = rotatedImage;\n      displayOCRBoxes(capturedImage, rotatedBoxes);\n      photoPreview.style.display = \"none\";\n\n      recognizedText = j.data.text || document.getElementById(\"no-text-recognized\").textContent;\n      let locale = j.data.locale || \"??\";\n      mainLangCode = locale.length >= 2 ? locale.substr(0, 2).toLowerCase() : \"de\";\n\n      const lang = document.documentElement.lang.startsWith('en') ? 'en' : 'de';\n      const languageNames = new Intl.DisplayNames([lang], { type: 'language' });\n      languageStatus.textContent = `${getTranslation(\"detected-language-prefix\")} ${languageNames.of(mainLangCode)}`;\n      languageStatus.style.color = \"green\";\n\n      \/\/ 1. ERST alle Dropdowns aufbauen\n      resetFilters();\n      buildDialectList(mainLangCode);\n      \n      genderSelect.value = \"\";\n      genderSelect.parentElement.style.display = \"block\";\n      \n      qualitySelect.innerHTML = `<option value=\"\" disabled selected>${getTranslation('select-quality')}<\/option>`;\n      qualitySelect.parentElement.style.display = \"none\";\n      \n      voiceSelect.innerHTML = `<option value=\"\" disabled selected>${getTranslation('select-voice')}<\/option>`;\n      \n      selectionContainer.style.display = \"block\";\n      document.getElementById(\"voiceSettingsHint\").style.display = \"block\";\n\n      \/\/ 2. DANN warten bis Dropdowns fertig sind\n      await new Promise(resolve => setTimeout(resolve, 200));\n\n      \/\/ 3. User-Settings ODER Default-Voice anwenden\n      const ocrDialect = locale.match(\/^([a-zA-Z]{2}-[a-zA-Z]+)\/) ? locale : mainLangCode;\n      \n      \/\/ Pr\u00fcfe ob User gespeicherte Settings hat\n      const hasUserSettings = userVoiceSettings[ocrDialect] || \n                            Object.keys(userVoiceSettings).find(k => k.toLowerCase().startsWith(ocrDialect.toLowerCase() + \"-\"));\n      \n      if (hasUserSettings) {\n        applyUserVoiceSettings(ocrDialect);\n      } else {\n        \/\/ Keine User-Settings -> automatisch Standard-Stimme w\u00e4hlen\n        await autoSelectDefaultVoice(mainLangCode);\n      }\n\n      \/\/ 4. FINAL: \u00dcbersetzung anwenden\n      setTimeout(() => {\n        translateVoiceOptions();\n      }, 300);\n\n      \/\/ Rest des Codes...\n      const recognizedTextContainer = document.getElementById('recognizedTextContainer');\n      const recognizedTextContent = document.getElementById('recognizedTextContent');\n      const paragraphs = recognizedText\n        .split(\/\\n\\s*\\n\/)\n        .map(p => p.trim())\n        .filter(Boolean);\n\n      recognizedTextContent.innerHTML = paragraphs\n        .map(p => `<p>${p.replace(\/\\n\/g, '<br>')}<\/p>`)\n        .join('\\n');\n      recognizedTextContainer.style.display = 'block';\n\n      playTextButton.disabled = false;\n      shareTextButton.disabled = false;\n      copyTextButton.disabled = false;\n      saveAudioButton.disabled = false;\n      saveTextDocButton.disabled = false;\n      output.style.display = \"block\";\n\n      loadingIndicator.style.display = \"none\";\n    });\n\n  } catch (err) {\n    languageStatus.textContent = document.getElementById(\"ocr-error\").textContent;\n    languageStatus.style.color = \"red\";\n    console.error(err);\n    loadingIndicator.style.display = \"none\";\n  }\n});\n\nsaveAudioButton.addEventListener(\"click\", async () => {\n    if (!ttsAudio || !ttsAudio.src || !ttsAudio.src.startsWith(\"data:audio\")) {\n        return alert(getTranslation(\"play-tts-first\"));\n    }\n\n    let fileName = prompt(getTranslation(\"audio-filename-prompt\"), \"my-audio\");\n    if (!fileName) {\n        return;\n    }\n\n    try {\n        const audioData = ttsAudio.src.split(',')[1];\n\n        if (!audioData) {\n            throw new Error(\"Keine g\u00fcltigen Audiodaten verf\u00fcgbar.\");\n        }\n\n        const formData = new URLSearchParams({\n            action: \"save_audio\",\n            audio_data: audioData,\n            audio_filename: fileName\n        });\n\n        const resp = await fetch(ajaxurl, {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\n            body: formData,\n            credentials: \"include\"\n        });\n\n        const result = await resp.json();\n        if (!result.success) {\n            throw new Error(result.data || \"Unbekannter Fehler beim Speichern des Audios\");\n        }\n\n        audioSaveNotice.style.display = \"block\";\n        audioSaveNotice.textContent = \"Audio saved!\";\n        setTimeout(() => {\n            audioSaveNotice.style.display = \"none\";\n        }, 3000);\n\n    } catch (err) {\n        alert(getTranslation(\"audio-save-error\"));\n        console.error(err);\n    }\n});\n\n\n\nfunction renderImageToCanvas(base64, callback) {\n  const img = new Image();\n  img.onload = function () {\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = img.width;\n    canvas.height = img.height;\n    const ctx = canvas.getContext(\"2d\");\n    ctx.drawImage(img, 0, 0);\n    const finalImage = canvas.toDataURL(\"image\/jpeg\", 0.95);\n    callback(finalImage);\n  };\n  img.src = base64;\n}\n\n\n\n\/\/ Automatische Rotationsanalyse mit boundingPoly von Google Vision API\n\/\/ Zuverl\u00e4ssige Rotationserkennung\nfunction detectCorrectRotation(boxes) {\n  if (!boxes || boxes.length === 0) return 0;\n\n  const vertices = boxes[0].vertices;\n\n  const dx = vertices[1].x - vertices[0].x;\n  const dy = vertices[1].y - vertices[0].y;\n\n  const angleRadians = Math.atan2(dy, dx);\n  const angleDegrees = angleRadians * (180 \/ Math.PI);\n\n  \/\/ Normalize angle\n  const normalizedAngle = (angleDegrees + 360) % 360;\n\n  \/\/ Entscheidung treffen (Toleranz \u00b115\u00b0)\n  if ((normalizedAngle > 75 && normalizedAngle < 105)) {\n    return 90;\n  } else if ((normalizedAngle > 165 && normalizedAngle < 195)) {\n    return 180;\n  } else if ((normalizedAngle > 255 && normalizedAngle < 285)) {\n    return 270;\n  }\n\n  return 0; \/\/ keine Rotation n\u00f6tig\n}\n\n\n\/\/ Korrigierte Funktion autoRotateImage ohne Nutzung von photoPreview!\nfunction autoRotateImage(imageSrc, rotationDegrees, callback) {\n    const img = new Image();\n    img.onload = function() {\n        const canvas = document.createElement('canvas');\n        const ctx = canvas.getContext('2d');\n\n        if (rotationDegrees === 90 || rotationDegrees === 270) {\n            canvas.width = img.height;\n            canvas.height = img.width;\n        } else {\n            canvas.width = img.width;\n            canvas.height = img.height;\n        }\n\n        ctx.translate(canvas.width \/ 2, canvas.height \/ 2);\n        ctx.rotate(-rotationDegrees * Math.PI \/ 180);\n        ctx.drawImage(img, -img.width \/ 2, -img.height \/ 2);\n\n        const rotatedDataUrl = canvas.toDataURL('image\/jpeg', 0.9);\n\n        if (typeof callback === \"function\") {\n            callback(rotatedDataUrl);\n        }\n    };\n    img.src = imageSrc;\n}\n\n\nfunction rotateBoundingBoxes(boxes, imgWidth, imgHeight, rotationDegrees) {\n  let newWidth = imgWidth, newHeight = imgHeight;\n\n  if (rotationDegrees === 90 || rotationDegrees === 270) {\n    newWidth = imgHeight;\n    newHeight = imgWidth;\n  }\n\n  return boxes.map(box => {\n    const rotatedVertices = box.vertices.map(v => {\n      switch(rotationDegrees) {\n        case 90: return { x: v.y, y: imgWidth - v.x };\n        case 180: return { x: imgWidth - v.x, y: imgHeight - v.y };\n        case 270: return { x: imgHeight - v.y, y: v.x };\n        default: return { x: v.x, y: v.y };\n      }\n    });\n    return { ...box, vertices: rotatedVertices };\n  });\n}\n\n\n  function resetFilters() {\n    selectedDialect = \"\";\n    selectedGender  = \"\";\n    selectedQuality = \"\";\n    dialectSelect.value = \"\";\n    genderSelect.value = \"\";\n    qualitySelect.value = \"\";\n    voiceSelect.value = \"\";\n  }\n\n  \/* 5) Dialekt \/ Geschlecht \/ Qualit\u00e4t \/ Stimme *\/\n  function buildDialectList(lang2) {\n    const allCodes = Object.keys(availableVoices);\n    const matched  = allCodes.filter(c => c.toLowerCase().startsWith(lang2 + \"-\"));\n\n    dialectSelect.innerHTML = '<option value=\"\" disabled selected>Dialekt ausw\u00e4hlen<\/option>';\n\n    if (matched.length === 0) {\n      dialectSelect.parentElement.style.display = \"none\";\n      return;\n    }\n    dialectSelect.parentElement.style.display = \"block\";\n    matched.forEach(code => {\n      const opt = document.createElement(\"option\");\n      opt.value = code;\n      opt.textContent = availableVoices[code].languageName || code;\n      dialectSelect.appendChild(opt);\n    });\n  }\n\n  dialectSelect.addEventListener(\"change\", () => {\n    selectedDialect = dialectSelect.value;\n    selectedGender  = \"\";\n    selectedQuality = \"\";\n    genderSelect.value = \"\";\n    qualitySelect.value= \"\";\n    rebuildQualityAndVoices();\n  });\n\n  genderSelect.addEventListener(\"change\", () => {\n    selectedGender = genderSelect.value;\n    selectedQuality= \"\";\n    qualitySelect.value= \"\";\n    rebuildQualityAndVoices();\n  });\n\n  qualitySelect.addEventListener(\"change\", () => {\n    selectedQuality = qualitySelect.value;\n    rebuildVoiceList();\n  });\n\n  function rebuildQualityAndVoices() {\n    buildQualityList();\n    rebuildVoiceList();\n  }\n\n  function buildQualityList() {\n    qualitySelect.innerHTML = '<option value=\"\" disabled selected>Qualit\u00e4t ausw\u00e4hlen<\/option>';\n    \n    if (!selectedDialect || !availableVoices[selectedDialect]) return;\n\n    let vs = collectVoicesForDialect(selectedDialect);\n    vs = filterByGender(vs, selectedGender);\n    const qualities = [...new Set(vs.map(v => v.quality))];\n    if (!qualities.length) return;\n\n    qualitySelect.parentElement.style.display = \"block\";\n    qualities.forEach(q => {\n      const opt = document.createElement(\"option\");\n      opt.value = q;\n      opt.textContent = q;\n      qualitySelect.appendChild(opt);\n    });\n  }\n\nfunction rebuildVoiceList() {\n  voiceSelect.innerHTML = `<option value=\"\" disabled selected>${getTranslation('select-voice')}<\/option>`;\n  if (!selectedDialect || !availableVoices[selectedDialect]) return;\n\n  let voices = collectVoicesForDialect(selectedDialect);\n  voices = filterByGender(voices, selectedGender);\n  voices = filterByQuality(voices, selectedQuality);\n\n  const lang = document.documentElement.lang.startsWith('en') ? 'en' : 'de';\n  const qualityTranslations = {\n    'standard': lang === 'en' ? 'Standard' : 'Standard',\n    'wavenet': 'WaveNet',\n    'studio': 'Studio',\n    'neural2': 'Neural2',\n    'journey': 'Journey',\n    'news': 'News',\n    'polyglot': 'Polyglot',\n    'casual': 'Casual'\n  };\n\n  if (!voices.length) {\n    voiceSelect.innerHTML = `<option value=\"\" disabled selected>${getTranslation('no-voice-available')}<\/option>`;\n    return;\n  }\n\n  voices.forEach(voice => {\n    const opt = document.createElement(\"option\");\n    const valObj = { dial: selectedDialect, voiceId: voice.voiceId };\n    opt.value = JSON.stringify(valObj);\n\n    const translatedQuality = qualityTranslations[voice.quality.toLowerCase()] || voice.quality;\n\n    opt.textContent = `${voice.voiceId} [${translatedQuality}]`; \/\/ Geschlecht entfernt!\n    voiceSelect.appendChild(opt);\n  });\n\n  updateVoiceDropdownAccessibility();\n}\n\n\n  function collectVoicesForDialect(dial) {\n    let arr = [];\n    const dialsObj = availableVoices[dial].dialects;\n    Object.keys(dialsObj).forEach(dName => {\n      arr = arr.concat(dialsObj[dName]);\n    });\n    return arr;\n  }\n\n  function unifyGender(g) {\n    if (!g) return \"\";\n    const low = g.toLowerCase();\n    if ([\"male\",\"m\u00e4nnlich\"].includes(low)) return \"male\";\n    if ([\"female\",\"weiblich\"].includes(low)) return \"female\";\n    return low;\n  }\n\n  function filterByGender(voices, g) {\n    if (!g) return voices;\n    return voices.filter(v => unifyGender(v.gender) === unifyGender(g));\n  }\n\n  function filterByQuality(arr, q) {\n    if (!q) return arr;\n    return arr.filter(v => v.quality === q);\n  }\n\n  \/* 6) userVoiceSettings \u00fcbernehmen *\/\n    function applyUserVoiceSettings(ocrDial) {\n  \/\/ 1. Exakten Key pr\u00fcfen (z.B. de-DE)\n  let uset = userVoiceSettings[ocrDial];\n  let usedDial = ocrDial;\n\n  \/\/ 2. Wenn nicht vorhanden: Partial-Match (z.B. de f\u00fcr de-DE, de-AT)\n  if (!uset) {\n    const allKeys = Object.keys(userVoiceSettings);\n    \/\/ Findet den ersten Key, der mit ocrDial- beginnt\n    const partialMatch = allKeys.find(k => k.toLowerCase().startsWith(ocrDial.toLowerCase() + \"-\"));\n    if (partialMatch) {\n      usedDial = partialMatch;\n      uset = userVoiceSettings[partialMatch];\n    }\n  }\n  if (!uset) return;\n\n    dialectSelect.value = usedDial;\n    selectedDialect = usedDial;\n\n    if (uset.gender) {\n      selectedGender = uset.gender;\n      genderSelect.value = uset.gender;\n    }\n\n    if (uset.quality) {\n      selectedQuality = uset.quality;\n    }\n    rebuildQualityAndVoices();\n\n    if (uset.quality) {\n      qualitySelect.value = uset.quality;\n    }\n    rebuildVoiceList();\n\n    if (uset.voiceId) {\n      setTimeout(() => {\n        for (let i = 0; i < voiceSelect.options.length; i++) {\n          const rawVal = voiceSelect.options[i].value;\n          if (!rawVal.trim()) continue;\n          let val;\n          try {\n            val = JSON.parse(rawVal);\n          } catch(e) {\n            continue;\n          }\n          if (val.voiceId === uset.voiceId) {\n            voiceSelect.selectedIndex = i;\n            break;\n          }\n        }\n      }, 50);\n    }\n  }\n\n  \/* 7) TTS \/ Text Vorlesen *\/\n\/* 7) TTS \/ Text Vorlesen mit Chirp\/Chirp3 Check *\/\nplayTextButton.addEventListener(\"click\", () => {\n  if (!recognizedText || recognizedText === \"Kein Text erkannt.\") {\n    return alert(getTranslation(\"no-text-to-play\"));\n  }\n  showFullscreenTTS(recognizedText);\n});\n\n\/\/ NEUE Hilfsfunktion um zu pr\u00fcfen, ob Stimme Chirp\/Chirp3 ist\nfunction isChirpVoice(voiceId) {\n  return voiceId.includes('-Chirp') || voiceId.includes('-Chirp3');\n}\n\n\/\/ === NEUER FULLSCREEN PLAY HANDLER MIT CHUNKING, CREDIT-CHECK & AUTO-SWITCH ===\ndocument.getElementById('fullscreenPlay').addEventListener('click', async function () {\n    const playButton = this;\n    const lang = document.documentElement.lang.startsWith('en') ? 'en' : 'de';\n    const pauseButton = fullscreenPause;\n    const stopButton = fullscreenStop;\n\n    \/\/ Globale Steuerungsvariablen\n    if (typeof window.ttsAudioObj === \"undefined\") window.ttsAudioObj = null;\n    if (typeof window.currentChunkIndex === \"undefined\") window.currentChunkIndex = 0;\n    if (typeof window.chunks === \"undefined\") window.chunks = [];\n    if (typeof window.isPaused === \"undefined\") window.isPaused = false;\n\n    \/\/ Hilfsfunktion Upgrade-Popup\n    function showUpgradePopup(msg) {\n        const upgradeText = lang === 'en' ? 'Upgrade now' : 'Jetzt upgraden';\n        const upgradeUrl = 'https:\/\/textsnapper.com\/pricing\/';\n        const popup = document.createElement('div');\n        popup.style.position = 'fixed';\n        popup.style.top = '0';\n        popup.style.left = '0';\n        popup.style.width = '100%';\n        popup.style.height = '100%';\n        popup.style.backgroundColor = 'rgba(0,0,0,0.7)';\n        popup.style.display = 'flex';\n        popup.style.flexDirection = 'column';\n        popup.style.justifyContent = 'center';\n        popup.style.alignItems = 'center';\n        popup.style.zIndex = '10000';\n        popup.innerHTML = `\n          <div style=\"background:#fff;padding:20px;border-radius:8px;max-width:400px;text-align:center;\">\n            <p style=\"margin-bottom:20px;\">${msg}<\/p>\n            <a href=\"${upgradeUrl}\" style=\"display:inline-block;padding:10px 20px;background:#2F5591;color:#fff;border-radius:5px;text-decoration:none;font-weight:bold;\">${upgradeText}<\/a>\n            <button style=\"margin-top:15px;background:none;border:none;color:#888;cursor:pointer;\" onclick=\"this.parentElement.parentElement.remove()\">Schlie\u00dfen<\/button>\n          <\/div>\n        `;\n        document.body.appendChild(popup);\n    }\n\n    \/\/ Chunk-Funktion (200 W\u00f6rter, 4950 Zeichen)\n    function createChunks(fullText) {\n        const sentences = fullText.match(\/[^.!?]+[.!?]+|[^.!?]+$\/g) || [fullText];\n        const chunks = [];\n        let current = '';\n        let wordCount = 0;\n        for (let s of sentences) {\n            const wordsInSentence = s.trim().split(\/\\s+\/).length;\n            if (\n                (wordCount + wordsInSentence > 200 && current.length > 0) ||\n                (current.length + s.length > 4950)\n            ) {\n                chunks.push(current.trim());\n                current = '';\n                wordCount = 0;\n            }\n            current += s;\n            wordCount += wordsInSentence;\n        }\n        if (current.trim().length > 0) chunks.push(current.trim());\n        return chunks;\n    }\n\n    \/\/ UI Reset\n    function resetButtons() {\n        pauseButton.style.display = 'none';\n        stopButton.style.display = 'none';\n        playButton.style.display = '';\n        playButton.textContent = getTranslation(\"play-audio\");\n    }\n\n    \/\/ ======== PAUSE\/RESUME LOGIK ========\n    if (window.ttsAudioObj && window.ttsAudioObj.paused && window.isPaused) {\n        window.ttsAudioObj.play();\n        window.isPaused = false;\n        pauseButton.style.display = '';\n        playButton.style.display = 'none';\n        stopButton.style.display = '';\n        return;\n    }\n\n    \/\/ ======== NEUSTART ========\n    window.chunks = [];\n    window.currentChunkIndex = 0;\n    window.ttsAudioObj = null;\n    window.isPaused = false;\n\n    await playChunk(0);\n\n    \/\/ ========== CHUNK PLAYER ==========\n    async function playChunk(index) {\n        \/\/ Lade Chunks beim ersten Start\n        if (!window.chunks.length) {\n            let text = fullscreenText.textContent;\n            if (!text || text === \"Kein Text erkannt.\") {\n                alert(getTranslation(\"no-text-to-play\"));\n                resetButtons();\n                return;\n            }\n            window.chunks = createChunks(text);\n        }\n        if (index >= window.chunks.length) {\n            resetButtons();\n            fullscreenText.textContent = window.chunks.join(\" \");\n            return;\n        }\n        \/\/ Credit-Check\n        let creditsResponse = await fetch(ajaxurl, {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\n            credentials: \"include\",\n            body: new URLSearchParams({ action: \"get_user_credit_count\" })\n        });\n        let creditsData = await creditsResponse.json();\n        if (!creditsData.success) {\n            alert(lang === 'en' ? 'Error fetching credits data.' : 'Fehler beim Abrufen der Credits.');\n            resetButtons();\n            return;\n        }\n        let monthlyCredits = parseInt(creditsData.data.monthly_credits_remaining, 10);\n        let packageCredits = parseInt(creditsData.data.package_credits_remaining, 10);\n        let totalCreditsAvailable = monthlyCredits + packageCredits;\n        let chunkText = window.chunks[index];\n        if (totalCreditsAvailable < chunkText.length) {\n            const msg = lang === 'en'\n                ? `\u26a0\ufe0f Insufficient credits. Monthly credits: ${monthlyCredits}, Package credits: ${packageCredits}.`\n                : `\u26a0\ufe0f Unzureichende Credits. Monatliche Credits: ${monthlyCredits}, Paket-Credits: ${packageCredits}.`;\n            showUpgradePopup(msg);\n            resetButtons();\n            return;\n        }\n\n        fullscreenText.textContent = chunkText;\n\n        playButton.textContent = getTranslation(\"loading-audio\");\n        playButton.disabled = true;\n        pauseButton.style.display = '';\n        stopButton.style.display = '';\n        playButton.style.display = 'none';\n        window.isPaused = false;\n\n        \/\/ Stimme & Optionen\n        const selVal = voiceSelect.value;\n        if (!selVal) {\n            playButton.disabled = false;\n            playButton.textContent = getTranslation(\"play-audio\");\n            alert(getTranslation(\"select-voice-alert\"));\n            return;\n        }\n        const parsed = JSON.parse(selVal);\n        const voiceQuality = getVoiceQualityByVoiceId(parsed.voiceId);\n        const speakingRate = parseFloat(speedSelect.value) || 1;\n        let ssmlText = isChirpVoice(parsed.voiceId) ? chunkText : `<speak>${chunkText}<\/speak>`;\n        let params = new URLSearchParams({\n            action: \"process_tts\",\n            voiceId: parsed.voiceId,\n            speakingRate,\n            voice_quality: voiceQuality\n        });\n        if (isChirpVoice(parsed.voiceId)) {\n            params.append('text', chunkText);\n        } else {\n            params.append('ssml', ssmlText);\n        }\n\n        try {\n            let resp = await fetch(ajaxurl, {\n                method: \"POST\",\n                headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\n                body: params\n            });\n            let j = await resp.json();\n            if (!j.success) throw new Error(j.data || \"Fehler bei TTS-Verarbeitung\");\n            window.ttsAudioObj = new Audio(`data:audio\/mp3;base64,${j.data.audioContent}`);\n\n            startSynchronizedHighlighting(window.ttsAudioObj, chunkText);\n\n            window.ttsAudioObj.addEventListener(\"ended\", function () {\n                if (window.isPaused) return;\n                window.currentChunkIndex++;\n                if (window.currentChunkIndex < window.chunks.length) {\n                    playChunk(window.currentChunkIndex);\n                } else {\n                    resetButtons();\n                    fullscreenText.textContent = window.chunks.join(\" \");\n                }\n            });\n            window.ttsAudioObj.addEventListener(\"error\", function () {\n                alert(\"Fehler bei der Audiowiedergabe.\");\n                resetButtons();\n            });\n\n            window.ttsAudioObj.play();\n            playButton.disabled = false;\n            pauseButton.style.display = '';\n            stopButton.style.display = '';\n            playButton.style.display = 'none';\n        } catch (err) {\n            alert(\"Fehler bei TTS: \" + (err && err.message ? err.message : JSON.stringify(err)));\n            resetButtons();\n        }\n    }\n\n    \/\/ ==== PAUSE BUTTON ====\n    pauseButton.onclick = function () {\n        if (window.ttsAudioObj && !window.ttsAudioObj.paused) {\n            window.ttsAudioObj.pause();\n            pauseHighlighting();\n            window.isPaused = true;\n            pauseButton.style.display = 'none';\n            playButton.style.display = '';\n            playButton.textContent = getTranslation(\"play-audio\");\n        }\n    };\n\n    \/\/ ==== RESUME BUTTON ====\n    playButton.onclick = async function () {\n        if (window.ttsAudioObj && window.ttsAudioObj.paused && window.isPaused) {\n            window.ttsAudioObj.play();\n            resumeHighlighting();\n            window.isPaused = false;\n            pauseButton.style.display = '';\n            playButton.style.display = 'none';\n            stopButton.style.display = '';\n        }\n    };\n\n    \/\/ ==== STOP BUTTON ====\n    stopButton.onclick = function () {\n        if (window.ttsAudioObj) {\n            window.ttsAudioObj.pause();\n            window.ttsAudioObj.currentTime = 0;\n            stopHighlighting();\n            window.isPaused = false;\n            resetButtons();\n            fullscreenText.textContent = window.chunks.join(\" \");\n        }\n    };\n\n});\n\n\nfunction toggleSavedVoices() {\n  const voicesList = document.getElementById('userSavedVoicesList');\n  const arrow = document.getElementById('toggleArrow');\n\n  if (voicesList.style.display === 'none' || voicesList.style.display === '') {\n    voicesList.style.display = 'block';\n    arrow.textContent = '\u25b2 zuklappen \u25b2';\n  } else {\n    voicesList.style.display = 'none';\n    arrow.textContent = '\u25bc aufklappen \u25bc';\n  }\n}\n\n  \n\/* 9) INIT *\/\ndocument.addEventListener(\"DOMContentLoaded\", async () => {\n  document.getElementById(\"processText\").style.display = \"none\";\n\n  fileUpload.setAttribute(\"capture\", \"environment\");\n  await fetchAvailableVoices();\n  await fetchGoogleVoices();\n  await fetchUserSavedVoices();\n  updateVoiceDropdownAccessibility();\n\n  await applyManualTranslations(); \n  \n  document.getElementById(\"startCamera\").textContent = '\ud83d\udcf7 ' + getTranslation(\"start-camera\");\n  document.getElementById(\"uploadImage\").textContent = '\ud83d\udcc1 ' + getTranslation(\"upload-image\");\n  document.getElementById(\"takePhoto\").textContent = '\ud83d\udcf8 ' + getTranslation(\"take-photo\");\n  document.getElementById(\"processText\").textContent = '\ud83d\udd0d ' + getTranslation(\"process-text\");\n  document.getElementById(\"playText\").textContent = '\ud83d\udd0a ' + getTranslation(\"play-text\");\n  document.getElementById(\"shareText\").textContent = '\ud83d\udce4 ' + getTranslation(\"share-text\");\n  document.getElementById(\"copyText\").textContent = '\ud83d\udccb ' + getTranslation(\"copy-text\");\n  document.getElementById(\"saveAudio\").textContent = '\ud83d\udcbe ' + getTranslation(\"save-audio\");\n  document.getElementById(\"saveTextDoc\").textContent = '\ud83d\udcbe ' + getTranslation(\"save-text-doc\");\n\n  document.querySelector('label[for=\"dialectSelect\"]').textContent = getTranslation(\"select-dialect-label\");\n  document.querySelector('label[for=\"genderSelect\"]').textContent = getTranslation(\"select-gender-label\");\n  document.querySelector('label[for=\"qualitySelect\"]').textContent = getTranslation(\"select-quality-label\");\n  document.querySelector('label[for=\"voiceSelect\"]').textContent = getTranslation(\"select-voice-label\");\n  document.querySelector('label[for=\"speedSelectValue\"]').textContent = getTranslation(\"select-speed-label\");\n\n  document.getElementById(\"languageStatus\").textContent = getTranslation(\"detected-language\");\n  document.getElementById(\"recognizedTextContainer\").querySelector(\"h3\").textContent = getTranslation(\"recognized-text-label\");\n  document.getElementById(\"voiceSettingsHint\").innerHTML = document.getElementById(`voice-settings-hint-${document.documentElement.lang.startsWith(\"en\") ? \"en\" : \"de\"}`).innerHTML;\n  document.getElementById(\"fullscreenPlay\").textContent = getTranslation(\"play-audio\");\ndocument.querySelector('.font-controls label[for=\"fontSizeSlider\"]').textContent = getTranslation(\"font-size\");\n\n  const updateDetectedLanguage = () => {\n    const langPrefix = getTranslation(\"detected-language-prefix\");\n    const detectedLangText = languageStatus.textContent.split(\": \")[1] || \"...\";\n    languageStatus.textContent = `${langPrefix} ${detectedLangText}`;\n  };\n\n  updateDetectedLanguage();\n  const speedDecreaseBtn = document.getElementById('speedDecrease');\n  const speedIncreaseBtn = document.getElementById('speedIncrease');\n  const speedSelectValue = document.getElementById('speedSelectValue');\n  const speedSelect = document.getElementById('speedSelect');\n  const speedLabel = document.getElementById('speedLabel');\n  let speed = 1.00;\n  const minSpeed = 0.50;\n  const maxSpeed = 2.00;\n  const step = 0.05;\n\n  if (speedLabel) speedLabel.textContent = getTranslation(\"select-speed-label\");\n\n  function updateSpeedDisplay() {\n    speed = Math.max(minSpeed, Math.min(maxSpeed, speed));\n    if (speedSelectValue) speedSelectValue.value = speed.toFixed(2);\n    if (speedSelect) speedSelect.value = speed.toFixed(2);\n  }\n\n  if (speedDecreaseBtn) {\n    speedDecreaseBtn.addEventListener('click', function () {\n      speed = Math.max(minSpeed, speed - step);\n      updateSpeedDisplay();\n    });\n  }\n\n  if (speedIncreaseBtn) {\n    speedIncreaseBtn.addEventListener('click', function () {\n      speed = Math.min(maxSpeed, speed + step);\n      updateSpeedDisplay();\n    });\n  }\n\n  updateSpeedDisplay();\n});\n\n\n\nasync function applyManualTranslations() {\n  const lang = document.documentElement.lang.startsWith(\"en\") ? \"en\" : \"de\";\n\n  const dropdowns = {\n    \"genderSelect\": [\n      { value: \"\", translationKey: \"select-gender\" },\n      { value: \"M\u00c4NNLICH\", translationKey: \"male\" },\n      { value: \"WEIBLICH\", translationKey: \"female\" }\n    ],\n    \"dialectSelect\": [\n      { value: \"\", translationKey: \"select-dialect\" }\n    ],\n    \"qualitySelect\": [\n      { value: \"\", translationKey: \"select-quality\" }\n    ],\n    \"voiceSelect\": [\n      { value: \"\", translationKey: \"select-voice\" }\n    ],\n    \"speedSelect\": [\n      { value: \"0.5\", translationKey: \"speed-slow\" },\n      { value: \"1\", translationKey: \"speed-normal\" },\n      { value: \"1.5\", translationKey: \"speed-fast\" },\n      { value: \"2\", translationKey: \"speed-very-fast\" }\n    ]\n  };\n\n  Object.entries(dropdowns).forEach(([selectId, options]) => {\n    const selectElement = document.getElementById(selectId);\n    if (!selectElement) return;\n\n    options.forEach(({ value, translationKey }) => {\n      let option;\n      if (value === \"\") {\n        option = selectElement.querySelector(`option[value=\"\"][data-translation-key=\"${translationKey}\"]`);\n      } else {\n        option = selectElement.querySelector(`option[value=\"${value}\"]`);\n      }\n\n      if (option) {\n        const translationElement = document.getElementById(`${translationKey}-${lang}`);\n        if (translationElement) {\n          option.textContent = translationElement.textContent;\n        }\n      }\n    });\n  });\n\n  \/\/ Labels zus\u00e4tzlich aktualisieren\n  const labels = {\n    \"dialectSelect\": \"select-dialect-label\",\n    \"genderSelect\": \"select-gender-label\",\n    \"qualitySelect\": \"select-quality-label\",\n    \"voiceSelect\": \"select-voice-label\",\n    \"speedSelect\": \"select-speed-label\",\n    \"fontSizeLabel\": \"font-size\"\n  };\n\n  Object.entries(labels).forEach(([selectId, labelTranslationKey]) => {\n    const labelElement = document.querySelector(`label[for=\"${selectId}\"]`);\n    const translationElement = document.getElementById(`${labelTranslationKey}-${lang}`);\n    if (labelElement && translationElement) {\n      labelElement.textContent = translationElement.textContent;\n    }\n  });\n\n  \/\/ Status- und Hinweis-Texte \u00fcbersetzen\n  document.getElementById(\"languageStatus\").textContent = getTranslation(\"detected-language\");\n  document.getElementById(\"recognizedTextContainer\").querySelector(\"h3\").textContent = getTranslation(\"recognized-text-label\");\n  document.getElementById(\"voiceSettingsHint\").innerHTML = document.getElementById(`voice-settings-hint-${lang}`).innerHTML;\n\n  \/\/ Buttons \u00fcbersetzen\n  document.getElementById(\"startCamera\").textContent = '\ud83d\udcf7 ' + getTranslation(\"start-camera\");\n  document.getElementById(\"uploadImage\").textContent = '\ud83d\udcc1 ' + getTranslation(\"upload-image\");\n  document.getElementById(\"takePhoto\").textContent = '\ud83d\udcf8 ' + getTranslation(\"take-photo\");\n  document.getElementById(\"processText\").textContent = '\ud83d\udd0d ' + getTranslation(\"process-text\");\n  document.getElementById(\"playText\").textContent = '\ud83d\udd0a ' + getTranslation(\"play-text\");\n  document.getElementById(\"shareText\").textContent = '\ud83d\udce4 ' + getTranslation(\"share-text\");\n  document.getElementById(\"copyText\").textContent = '\ud83d\udccb ' + getTranslation(\"copy-text\");\n  document.getElementById(\"saveAudio\").textContent = '\ud83d\udcbe ' + getTranslation(\"save-audio\");\n  document.getElementById(\"saveTextDoc\").textContent = '\ud83d\udcbe ' + getTranslation(\"save-text-doc\");\n  document.getElementById(\"fullscreenPlay\").textContent = getTranslation(\"play-audio\");\n  document.getElementById(\"fullscreenPlay\").setAttribute('data-loading-text', getTranslation(\"loading-audio\"));\n  document.getElementById(\"closeFullscreen\").textContent = getTranslation(\"back-button-fullscreen\");\n\n}\n\nasync function autoSelectDefaultVoice(langCode) {\n  console.log(`[AUTO-SELECT] Starte f\u00fcr Sprache: ${langCode}`);\n  \n  const dialects = Object.keys(availableVoices).filter(d => d.startsWith(langCode + \"-\"));\n  console.log(`[AUTO-SELECT] Gefundene Dialekte:`, dialects);\n\n  if (!dialects.length) {\n    console.log(`[AUTO-SELECT] Keine Dialekte f\u00fcr ${langCode} gefunden`);\n    return;\n  }\n\n  const firstDialect = dialects[0];\n  console.log(`[AUTO-SELECT] Verwende ersten Dialekt: ${firstDialect}`);\n  \n  \/\/ 1. Dialekt setzen\n  dialectSelect.value = firstDialect;\n  selectedDialect = firstDialect;\n  dialectSelect.dispatchEvent(new Event('change'));\n  await new Promise(r => setTimeout(r, 200));\n\n  \/\/ 2. ALLE verf\u00fcgbaren Stimmen dieses Dialekts holen\n  let voices = collectVoicesForDialect(firstDialect);\n  console.log(`[AUTO-SELECT] Alle Stimmen f\u00fcr ${firstDialect}:`, voices);\n\n  if (!voices.length) {\n    console.log(`[AUTO-SELECT] Keine Stimmen f\u00fcr Dialekt ${firstDialect} gefunden`);\n    return;\n  }\n\n  \/\/ 3. Priorisierung: Standard-Qualit\u00e4t bevorzugen\n  let standardVoices = voices.filter(v => v.quality.toLowerCase() === \"standard\");\n  let chosenVoice;\n  \n  if (standardVoices.length > 0) {\n    \/\/ Bevorzuge weibliche Standard-Stimmen, falls vorhanden\n    let femaleStandard = standardVoices.filter(v => unifyGender(v.gender) === \"female\");\n    chosenVoice = femaleStandard.length > 0 ? femaleStandard[0] : standardVoices[0];\n    console.log(`[AUTO-SELECT] Standard-Stimme gew\u00e4hlt:`, chosenVoice);\n  } else {\n    \/\/ Fallback: Erste verf\u00fcgbare Stimme (beliebige Qualit\u00e4t)\n    chosenVoice = voices[0];\n    console.log(`[AUTO-SELECT] Fallback-Stimme gew\u00e4hlt:`, chosenVoice);\n  }\n\n  \/\/ 4. Geschlecht setzen\n  const genderValue = chosenVoice.gender.toUpperCase();\n  genderSelect.value = genderValue;\n  selectedGender = genderValue;\n  genderSelect.dispatchEvent(new Event('change'));\n  await new Promise(r => setTimeout(r, 200));\n\n  \/\/ 5. Qualit\u00e4t setzen\n  qualitySelect.value = chosenVoice.quality;\n  selectedQuality = chosenVoice.quality;\n  qualitySelect.dispatchEvent(new Event('change'));\n  await new Promise(r => setTimeout(r, 200));\n\n  \/\/ 6. Stimme explizit setzen\n  const voiceOptions = voiceSelect.options;\n  for (let i = 1; i < voiceOptions.length; i++) { \/\/ Skip first disabled option\n    const optionValue = voiceOptions[i].value;\n    if (!optionValue.trim()) continue;\n    \n    try {\n      const parsedValue = JSON.parse(optionValue);\n      if (parsedValue.voiceId === chosenVoice.voiceId) {\n        voiceSelect.selectedIndex = i;\n        console.log(`[AUTO-SELECT] Stimme erfolgreich gesetzt: ${chosenVoice.voiceId} (Index: ${i})`);\n        break;\n      }\n    } catch(e) {\n      console.warn(`[AUTO-SELECT] Fehler beim Parsen der Option ${i}:`, e);\n      continue;\n    }\n  }\n\n  console.log(`[AUTO-SELECT] Finale Auswahl - Dialekt: ${firstDialect}, Geschlecht: ${genderValue}, Qualit\u00e4t: ${chosenVoice.quality}, Stimme: ${chosenVoice.voiceId}`);\n}\n\n  \/\/ Modal erzeugen und anzeigen\n  function showModal(contentHtml) {\n    const modal = document.createElement('div');\n    modal.classList.add('modal-overlay');\n    modal.innerHTML = `<div class=\"modal-content\">${contentHtml}<br><button class=\"close-modal\">Schlie\u00dfen<\/button><\/div>`;\n    document.body.appendChild(modal);\n\n    modal.querySelector('.close-modal').addEventListener('click', () => modal.remove());\n    modal.addEventListener('click', (e) => {\n      if (e.target === modal) modal.remove();\n    });\n\n    return modal;\n  }\n\n\n\n  \/\/ TTS-Funktion (Integration in dein vorhandenes System)\n  function playTTS(text) {\n    recognizedText = text;\n    document.getElementById('playText').click(); \/\/ Vorhandene TTS-Funktion aufrufen\n  }\n\nasync function displayOCRBoxes(imageSrc, boxes) {\n  const container = document.createElement('div');\n  container.className = 'ocr-image-container';\n\n  const img = document.createElement('img');\n  img.src = imageSrc;\n  container.appendChild(img);\n\n  const resultContainer = document.getElementById('ocr-result');\n  resultContainer.innerHTML = '';\n  resultContainer.appendChild(container);\n\n  img.onload = () => {\n    \/\/ Verwende ResizeObserver, um endg\u00fcltige Gr\u00f6\u00dfe sicherzustellen!\n    const resizeObserver = new ResizeObserver(entries => {\n      for (let entry of entries) {\n        const imgWidth = img.naturalWidth;\n        const imgHeight = img.naturalHeight;\n\n        \/\/ WICHTIG: Endg\u00fcltige gerenderte Gr\u00f6\u00dfe des Bildes!\n        const rect = img.getBoundingClientRect();\nconst scaleX = rect.width \/ img.naturalWidth;\nconst scaleY = rect.height \/ img.naturalHeight;\nconst offsetX = rect.left;\nconst offsetY = rect.top;\n\n        \/\/ L\u00f6sche zuerst bestehende OCR-Boxen (falls vorhanden)\n        container.querySelectorAll('.ocr-box').forEach(el => el.remove());\n\n        boxes.forEach(box => {\n          const vertices = box.vertices;\n          const minX = Math.min(...vertices.map(v => v.x));\n          const minY = Math.min(...vertices.map(v => v.y));\n          const maxX = Math.max(...vertices.map(v => v.x));\n          const maxY = Math.max(...vertices.map(v => v.y));\n\n          const boxDiv = document.createElement('div');\n          boxDiv.className = 'ocr-box';\n\n          \/\/ Positionen und Gr\u00f6\u00dfen responsive anpassen (jetzt korrekt!)\n          boxDiv.style.left   = `${(minX * scaleX)}px`;\nboxDiv.style.top    = `${(minY * scaleY)}px`;\n          boxDiv.style.width  = `${(maxX - minX) * scaleX}px`;\n          boxDiv.style.height = `${(maxY - minY) * scaleY}px`;\n\n          boxDiv.addEventListener('click', () => {\n            showModalWithText(box.text);\n          });\n\n          container.appendChild(boxDiv);\n        });\n\n        resizeObserver.disconnect();  \/\/ Observer nach einmaliger Anwendung trennen!\n      }\n    });\n\n    resizeObserver.observe(img);\n  };\n}\n\nfunction rotateImageWithBoxes(imageSrc, boxes, rotationDegrees, callback) {\n  const img = new Image();\n  img.onload = function () {\n    const canvas = document.createElement(\"canvas\");\n    const ctx = canvas.getContext(\"2d\");\n\n    const width = img.width;\n    const height = img.height;\n\n    if (rotationDegrees === 90 || rotationDegrees === 270) {\n      canvas.width = height;\n      canvas.height = width;\n    } else {\n      canvas.width = width;\n      canvas.height = height;\n    }\n\n    ctx.translate(canvas.width \/ 2, canvas.height \/ 2);\n    ctx.rotate(-rotationDegrees * Math.PI \/ 180);\n    ctx.drawImage(img, -width \/ 2, -height \/ 2);\n\n    const rotatedImage = canvas.toDataURL(\"image\/jpeg\", 0.9);\n    const rotatedBoxes = rotateBoundingBoxes(boxes, width, height, rotationDegrees);\n\n    if (typeof callback === \"function\") {\n      callback({ rotatedImage, rotatedBoxes });\n    }\n  };\n  img.src = imageSrc;\n}\n\n\nfunction showModalWithText(text){\n  showFullscreenTTS(text);\n}\n\n\n\/\/ --- Fullscreen Overlay anzeigen ---\nfunction showFullscreenTTS(text) {\n  document.getElementById('fullscreenText').textContent = text;\n  document.getElementById('fullscreenTTS').style.display = 'flex';\n  setTimeout(() => document.getElementById('fullscreenPlay').click(), 300);\n}\n\n\n\/\/ Fullscreen schlie\u00dfen\ndocument.getElementById('closeFullscreen').addEventListener('click', function(){\n  document.getElementById('fullscreenTTS').style.display = 'none';\n\n  \/\/ TTS wirklich stoppen (egal ob paused oder spielt)\n  if (window.ttsAudioObj) {\n    window.ttsAudioObj.pause();\n    window.ttsAudioObj.currentTime = 0;\n    window.ttsAudioObj = null;\n  }\n  window.isPaused = false;\n  window.currentChunkIndex = 0;\n  window.chunks = [];\n\n  \/\/ Highlighting zur\u00fccksetzen\n  stopHighlighting && stopHighlighting();\n  fullscreenText.innerHTML = '';\n\n  \/\/ UI-Buttons zur\u00fccksetzen\n  fullscreenPause.style.display = 'none';\n  fullscreenPlay.style.display = '';\n  fullscreenStop.style.display = 'none';\n\n  \/\/ Text zur\u00fccksetzen (optional: gesamten Text wieder anzeigen)\n  \/\/ fullscreenText.textContent = recognizedText || \"\";\n});\n\n\/\/ Fullscreen TTS (Play & Stop)\n\nlet originalFullText = \"\";\nlet highlightInterval = null;\nlet currentWordIndex = 0;\nlet wordsArr = [];\nlet wordCount = 0;\nlet avgWordDuration = 0;\n\nfunction startSynchronizedHighlighting(audio, fullText) {\n  wordsArr = fullText.split(\/\\s+\/);\n  wordCount = wordsArr.length;\n  originalFullText = fullText;\n\n  \/\/ 1. Baue das Text-HTML mit Spans f\u00fcr jedes Wort:\n  fullscreenText.innerHTML = wordsArr.map((word, idx) => `<span data-idx=\"${idx}\">${word}<\/span>`).join(' ');\n  const wordElements = fullscreenText.querySelectorAll('span');\n\n  \/\/ 2. Haupt-Logik: timeupdate-Event statt setInterval!\n  function highlightCurrentWord() {\n    if (!audio.duration || audio.paused) return;\n    \/\/ 3. Bestimme das aktuelle Wort auf Basis der Abspielposition:\n    const t = audio.currentTime;\n    const durationPerWord = audio.duration \/ wordCount;\n    let idx = Math.floor(t \/ durationPerWord);\n\n    \/\/ 4. Highlight-Fenster: -4 bis +8 um das aktuelle Wort herum:\n    const start = Math.max(0, idx - 4);\n    const end = Math.min(wordCount, idx + 8);\n\n    \/\/ 5. Alle Highlights zur\u00fccksetzen, dann Bereich setzen:\n    wordElements.forEach(el => el.classList.remove('highlight'));\n    for (let i = start; i < end; i++) {\n      wordElements[i].classList.add('highlight');\n    }\n    \/\/ 6. Automatisch zentrieren:\n    if (wordElements[idx]) {\n      wordElements[idx].scrollIntoView({ behavior: 'smooth', block: 'center' });\n    }\n  }\n\n  \/\/ 7. Event-Handler setzen:\n  audio.addEventListener('timeupdate', highlightCurrentWord);\n  audio.addEventListener('ended', () => {\n    wordElements.forEach(el => el.classList.remove('highlight'));\n    fullscreenText.innerHTML = originalFullText;\n  });\n  audio.addEventListener('pause', () => {\n    \/\/ beim Pause keine Highlights entfernen \u2013 so bleibt der Stand sichtbar\n  });\n}\n\n\nfunction pauseHighlighting() {\n  clearInterval(highlightInterval);\n}\nfunction resumeHighlighting() {\n  highlightInterval = setInterval(() => {\n    if (ttsAudio.paused || ttsAudio.ended || currentWordIndex >= wordCount) {\n      clearInterval(highlightInterval);\n      fullscreenText.innerHTML = originalFullText;\n      return;\n    }\n    highlightWords(currentWordIndex);\n    currentWordIndex++;\n  }, avgWordDuration * 1000);\n}\nfunction stopHighlighting() {\n  clearInterval(highlightInterval);\n  fullscreenText.innerHTML = originalFullText;\n}\n\nfunction highlightWords(index) {\n  const startIdx = Math.max(0, index - 4);\n  const endIdx = Math.min(wordCount, index + 8);\n  fullscreenText.innerHTML =\n    wordsArr.slice(0, startIdx).join(' ') +\n    ' <span class=\"highlight\">' +\n    wordsArr.slice(startIdx, endIdx).join(' ') +\n    '<\/span> ' +\n    wordsArr.slice(endIdx).join(' ');\n\n  const highlightEl = fullscreenText.querySelector('.highlight');\n  if (highlightEl) {\n    highlightEl.scrollIntoView({ behavior: 'smooth', block: 'center' });\n  }\n}\n\n\n\n\nfontSizeSlider.addEventListener('input', () => {\n  fullscreenText.style.fontSize = `${fontSizeSlider.value}px`;\n});\n\n\/\/ Text hervorheben w\u00e4hrend der Wiedergabe\nasync function highlightSpokenText(audio, fullText) {\n  const words = fullText.split(\/\\s+\/);\n  fullscreenText.innerHTML = words.map(word => `<span>${word}<\/span>`).join(' ');\n\n  const wordElements = fullscreenText.querySelectorAll('span');\n  const totalDuration = audio.duration;\n  const averageDurationPerWord = totalDuration \/ words.length;\n\n  let currentIndex = 0;\n\n  function highlightCurrentWord(index) {\n    wordElements.forEach(el => el.classList.remove('highlight'));\n\n    const startHighlight = Math.max(index - 4, 0);\n    const endHighlight = Math.min(index + 8, words.length - 1);\n\n    for (let i = startHighlight; i <= endHighlight; i++) {\n      wordElements[i].classList.add('highlight');\n    }\n\n    \/\/ Scroll automatisch zur aktuellen Hervorhebung\n    wordElements[index].scrollIntoView({ behavior: 'smooth', block: 'center' });\n  }\n\n  const interval = setInterval(() => {\n    if (audio.paused || audio.ended) {\n      clearInterval(interval);\n      wordElements.forEach(el => el.classList.remove('highlight'));\n      return;\n    }\n    highlightCurrentWord(currentIndex++);\n    if (currentIndex >= words.length) {\n      clearInterval(interval);\n    }\n  }, averageDurationPerWord * 1000);\n}\n\n\/\/ NEU: Text & Thumbnail als Dokument speichern\nsaveTextDocButton.addEventListener(\"click\", async () => {\n    if (!recognizedText || recognizedText === \"Kein Text erkannt.\") {\n        return alert(getTranslation(\"no-text-to-save\"));\n    }\n\n    let fileName = prompt(getTranslation(\"text-filename-prompt\"), \"my-text\");\n\n    \/\/ Wichtig: Wenn Nutzer Abbrechen klickt oder kein Dateiname eingegeben wird\n    if (!fileName) {\n        return;  \/\/ Beendet die Funktion hier sofort!\n    }\n\n    try {\n        const resp = await fetch(ajaxurl, {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\n            body: new URLSearchParams({\n                action: \"save_txt_with_thumbnail\",\n                text_data: recognizedText,\n                text_filename: fileName,\n                image_base64: capturedImage\n            })\n        });\n\n        const j = await resp.json();\n        if (!j.success) throw new Error(j.data);\n\n        audioSaveNotice.style.display = \"block\";\n        audioSaveNotice.textContent = \"Text saved!\";\n    } catch (err) {\n        alert(\"Fehler beim Speichern des Dokuments.\");\n        console.error(err);\n    }\n});\n\n\/\/ Kopieren Button\ncopyTextButton.addEventListener(\"click\", () => {\n  if (!recognizedText || recognizedText === \"Kein Text erkannt.\") {\n    alert(getTranslation(\"nothing-to-copy\"));\n    return;\n  }\n  \n  \/\/ Text in Zwischenablage kopieren\n  if (navigator.clipboard && navigator.clipboard.writeText) {\n    navigator.clipboard.writeText(recognizedText)\n      .then(() => {\n        \/\/ Erfolgsmeldung anzeigen\n        audioSaveNotice.style.display = \"block\";\n        audioSaveNotice.textContent = getTranslation(\"text-copied\");\n        audioSaveNotice.style.color = \"green\";\n        setTimeout(() => {\n          audioSaveNotice.style.display = \"none\";\n        }, 3000);\n      })\n      .catch(err => {\n        \/\/ Fallback f\u00fcr \u00e4ltere Browser\n        const textArea = document.createElement(\"textarea\");\n        textArea.value = recognizedText;\n        textArea.style.position = \"fixed\";\n        textArea.style.left = \"-999999px\";\n        document.body.appendChild(textArea);\n        textArea.select();\n        try {\n          document.execCommand('copy');\n          audioSaveNotice.style.display = \"block\";\n          audioSaveNotice.textContent = getTranslation(\"text-copied\");\n          audioSaveNotice.style.color = \"green\";\n          setTimeout(() => {\n            audioSaveNotice.style.display = \"none\";\n          }, 3000);\n        } catch (err) {\n          alert(\"Kopieren fehlgeschlagen\");\n        }\n        document.body.removeChild(textArea);\n      });\n  } else {\n    \/\/ Fallback f\u00fcr sehr alte Browser\n    const textArea = document.createElement(\"textarea\");\n    textArea.value = recognizedText;\n    textArea.style.position = \"fixed\";\n    textArea.style.left = \"-999999px\";\n    document.body.appendChild(textArea);\n    textArea.select();\n    try {\n      document.execCommand('copy');\n      audioSaveNotice.style.display = \"block\";\n      audioSaveNotice.textContent = getTranslation(\"text-copied\");\n      audioSaveNotice.style.color = \"green\";\n      setTimeout(() => {\n        audioSaveNotice.style.display = \"none\";\n      }, 3000);\n    } catch (err) {\n      alert(\"Kopieren fehlgeschlagen\");\n    }\n    document.body.removeChild(textArea);\n  }\n});\n\n\/\/ Teilen Button\nshareTextButton.addEventListener(\"click\", async () => {\n  if (!recognizedText || recognizedText === \"Kein Text erkannt.\") {\n    alert(getTranslation(\"no-text-to-share\"));\n    return;\n  }\n  \n  \/\/ Web Share API verwenden (falls verf\u00fcgbar)\n  if (navigator.share) {\n    try {\n      await navigator.share({\n        title: 'OCR Text',\n        text: recognizedText\n      });\n    } catch (err) {\n      \/\/ Benutzer hat Teilen abgebrochen - das ist OK\n      if (err.name !== 'AbortError') {\n        console.error('Fehler beim Teilen:', err);\n      }\n    }\n  } else {\n    \/\/ Fallback: Text kopieren und Hinweis anzeigen\n    if (navigator.clipboard && navigator.clipboard.writeText) {\n      navigator.clipboard.writeText(recognizedText)\n        .then(() => {\n          alert(getTranslation(\"share-not-supported\") + \"\\n\\nText wurde in die Zwischenablage kopiert!\");\n        })\n        .catch(() => {\n          alert(getTranslation(\"share-not-supported\"));\n        });\n    } else {\n      \/\/ Sehr alter Browser - manuelles Kopieren\n      const textArea = document.createElement(\"textarea\");\n      textArea.value = recognizedText;\n      textArea.style.position = \"fixed\";\n      textArea.style.left = \"-999999px\";\n      document.body.appendChild(textArea);\n      textArea.select();\n      try {\n        document.execCommand('copy');\n        alert(getTranslation(\"share-not-supported\") + \"\\n\\nText wurde in die Zwischenablage kopiert!\");\n      } catch (err) {\n        alert(getTranslation(\"share-not-supported\"));\n      }\n      document.body.removeChild(textArea);\n    }\n  }\n});\n\nfunction autoPrepareSSML(text, langCode) {\n  \/\/ Sonderzeichen sch\u00fctzen\n  text = text.replace(\/&\/g, '&amp;').replace(\/<\/g, '&lt;').replace(\/>\/g, '&gt;');\n\n  \/\/ Dezimalzahlen: Punkt oder Komma erkennen und formatieren\n  text = text.replace(\/\\b(\\d+)[.,](\\d+)\\b\/g, (_, intPart, decimalPart) => {\n    return `<say-as interpret-as=\"cardinal\">${intPart}<\/say-as> Komma <say-as interpret-as=\"cardinal\">${decimalPart}<\/say-as>`;\n  });\n\n  \/\/ Eindeutige Telefonnummern (mit + und\/oder Bindestrichen\/Leerzeichen)\n  text = text.replace(\n    \/(\\+\\d{1,3}[-\\s])?(\\d{2,5}[-\\s]){1,4}\\d{2,9}\/g, \n    '<say-as interpret-as=\"telephone\">$&<\/say-as>'\n  );\n\n  \/\/ Datumsangaben (TT.MM.JJJJ)\n  text = text.replace(\n    \/(\\d{1,2}\\.\\d{1,2}\\.\\d{2,4})\/g, \n    '<say-as interpret-as=\"date\" format=\"dmy\">$&<\/say-as>'\n  );\n\n  \/\/ Lange Zahlen (5 oder mehr Ziffern), die nicht Teil eines vorherigen Tags sind\n  text = text.replace(\n    \/\\b(?<!>)\\d{5,}\\b(?!<)\\b\/g, \n    '<say-as interpret-as=\"characters\">$&<\/say-as>'\n  );\n\n  \/\/ Kurze Zahlen (bis zu 4 Stellen)\n  text = text.replace(\n    \/\\b(?<!>)\\d{1,4}\\b(?!<)\\b\/g, \n    '<say-as interpret-as=\"cardinal\">$&<\/say-as>'\n  );\n\n  return `<speak>${text}<\/speak>`;\n}\n\n\n\n\/\/ Beispiel, wie du den AJAX-Request baust (Desktop-Modus)\nasync function speakDesktopMode(text, voiceId, langCode, speakingRate = 1.0) {\n  const ssml = autoPrepareSSML(text, langCode);\n\n  const response = await fetch(window.ajaxurl, {\n    method: \"POST\",\n    credentials: 'same-origin',\n    headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\n    body: new URLSearchParams({\n      action: \"process_tts\",\n      ssml: ssml,            \/\/ <-- Wichtig: jetzt SSML senden!\n      voiceId: voiceId,\n      speakingRate: speakingRate\n    })\n  });\n\n  const result = await response.json();\n  if(result.success) {\n    const audio = new Audio(`data:audio\/mp3;base64,${result.data.audioContent}`);\n    audio.play();\n  } else {\n    console.error(\"TTS Fehler:\", result.data);\n  }\n}\n\nasync function fetchUserLanguage() {\n  try {\n    const geoResp = await fetch(ajaxurl, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\n      body: new URLSearchParams({ action: \"detect_user_language\" })\n    });\n\n    const geoJson = await geoResp.json();\n    return geoJson.success ? geoJson.data.language : \"en-US\";\n  } catch (err) {\n    console.error('Geo-Detection Error:', err);\n    return \"en-US\";\n  }\n}\n\nasync function fetchUserLanguage() {\n  try {\n    const geoResp = await fetch(ajaxurl, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\n      body: new URLSearchParams({ action: \"detect_user_language\" })\n    });\n\n    const geoJson = await geoResp.json();\n    return geoJson.success ? geoJson.data.language : \"en-US\";\n  } catch (err) {\n    console.error('Geo-Detection Error:', err);\n    return \"en-US\";\n  }\n}\n\nasync function determineVoice(text, ocrLang) {\n  const isNumeric = \/^\\d+$\/.test(text.replace(\/\\s+\/g, ''));\n\n  if (isNumeric) {\n    mainLangCode = await fetchUserLanguage();\n  } else {\n    mainLangCode = ocrLang || 'en-US';\n  }\n\n  buildDialectList(mainLangCode);\n  await new Promise(resolve => setTimeout(resolve, 150));\n\n  \/\/ Dialekt automatisch ausw\u00e4hlen, falls verf\u00fcgbar\n  if (dialectSelect.options.length > 1) {\n    dialectSelect.selectedIndex = 1;\n    dialectSelect.dispatchEvent(new Event('change'));\n    selectedDialect = dialectSelect.value;\n  } else {\n    selectedDialect = mainLangCode;\n  }\n\n  \/\/ Dropdowns sichtbar machen\n  dialectSelect.parentElement.style.display = \"block\";\n  genderSelect.parentElement.style.display = \"block\";\n  qualitySelect.parentElement.style.display = \"block\";\n  voiceSelect.parentElement.style.display = \"block\";\n  selectionContainer.style.display = \"block\";\n  statusContainer.style.display = \"block\";\n\n  await new Promise(resolve => setTimeout(resolve, 150));\n\n  \/\/ **NEU:** Versuche zuerst explizit eine Standard-Stimme zu w\u00e4hlen!\n  await autoSelectDefaultVoice(mainLangCode);\n  await new Promise(resolve => setTimeout(resolve, 150));\n\n  \/\/ Pr\u00fcfe, ob nun bereits eine Stimme gesetzt ist.\n  if (voiceSelect.value && voiceSelect.value !== '') {\n    return; \/\/ Stimme erfolgreich gesetzt \u2013 kein weiterer Ablauf n\u00f6tig!\n  }\n\n  \/\/ Erst wenn keine Stimme gesetzt wurde, folgt der alte automatische Ablauf:\n  const genders = ['WEIBLICH', 'M\u00c4NNLICH'];\n  let genderSelected = false;\n  for (let gender of genders) {\n    genderSelect.value = gender;\n    genderSelect.dispatchEvent(new Event('change'));\n    await new Promise(resolve => setTimeout(resolve, 100));\n    if (voiceSelect.options.length > 1) {\n      genderSelected = true;\n      selectedGender = gender;\n      break;\n    }\n  }\n\n  if (!genderSelected) {\n    selectedGender = \"\";\n  }\n\n  await new Promise(resolve => setTimeout(resolve, 100));\n  if (qualitySelect.options.length > 1) {\n    qualitySelect.selectedIndex = 1;\n    qualitySelect.dispatchEvent(new Event('change'));\n    selectedQuality = qualitySelect.value;\n  } else {\n    selectedQuality = \"\";\n  }\n\n  await new Promise(resolve => setTimeout(resolve, 100));\n  if (voiceSelect.options.length > 1) {\n    voiceSelect.selectedIndex = 1;\n  }\n\n  \/\/ Gespeicherte Einstellungen anwenden, falls vorhanden\n  applyUserVoiceSettings(mainLangCode);\n}\n\nfunction getVoiceQualityByVoiceId(voiceId) {\n    const foundVoice = googleVoicesList.find(v => v.code2 === voiceId);\n    return foundVoice ? foundVoice.Qualit\u00e4t.toLowerCase() : 'standard';\n}\n\nasync function checkOCRLimitForTTSWidget() {\n  const response = await fetch('https:\/\/textsnapper.com\/wp-admin\/admin-ajax.php', {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\n    credentials: \"include\",\n    body: new URLSearchParams({ action: \"get_user_ocr_count\" })\n  });\n  \n  const result = await response.json();\n\n  if (result.success) {\n    const { used_ocr, ocr_limit } = result.data;\n\n    if (ocr_limit !== -1 && used_ocr >= ocr_limit) {\n      \/\/ Buttons deaktivieren\n      document.getElementById('startCamera').disabled = true;\n      document.getElementById('uploadImage').disabled = true;\n\n      \/\/ Buttons optisch deaktivieren\n      document.getElementById('startCamera').style.background = '#ccc';\n      document.getElementById('uploadImage').style.background = '#ccc';\n      document.getElementById('startCamera').style.cursor = 'not-allowed';\n      document.getElementById('uploadImage').style.cursor = 'not-allowed';\n\n      \/\/ Hinweis hinzuf\u00fcgen (automatische \u00dcbersetzung)\n      const lang = document.documentElement.lang.startsWith('en') ? 'en' : 'de';\n      const upgradeText = lang === 'en' \n        ? '\u26a0\ufe0f No uploads remaining. Please upgrade your account.' \n        : '\u26a0\ufe0f Keine Uploads mehr m\u00f6glich. Bitte Upgrade durchf\u00fchren.';\n\n      const upgradeNotice = document.createElement('div');\n      upgradeNotice.style.color = '#b00000';\n      upgradeNotice.style.fontWeight = 'bold';\n      upgradeNotice.style.marginTop = '15px';\n      upgradeNotice.textContent = upgradeText;\n\n      document.querySelector('#ocr-tts-widget .controls-top').prepend(upgradeNotice);\n    }\n  } else {\n    console.error('Fehler bei OCR-Limitabfrage:', result.data);\n  }\n}\n\n\/\/ Nach dem DOM-Laden OCR-Limit pr\u00fcfen\ndocument.addEventListener('DOMContentLoaded', checkOCRLimitForTTSWidget);\n\nasync function fetchUserCredits() {\n  const response = await fetch(ajaxurl, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\n    credentials: \"include\",\n    body: new URLSearchParams({ action: \"get_user_credit_count\" })\n  });\n\n  const result = await response.json();\n  if (result.success) {\n    return parseInt(result.data.credits_remaining, 10);\n  } else {\n    console.error('Fehler beim Abrufen der Credits:', result.data);\n    return 0;\n  }\n}\n\n<\/script>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-e28e14e elementor-widget elementor-widget-text-editor\" data-id=\"e28e14e\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p>Click on <strong data-start=\"112\" data-end=\"132\">\"Start camera\"<\/strong>, take a photo of the desired text and confirm by clicking <strong data-start=\"186\" data-end=\"194\">\u201eOk\u201c<\/strong>.<br data-start=\"195\" data-end=\"198\" \/>You can choose to have the entire recognized text read aloud, or simply click on a red highlighted text box to play back just that section.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-ee33601 elementor-widget elementor-widget-html\" data-id=\"ee33601\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<div id=\"credit-count-notice-widget\" style=\"text-align:center; margin-top:20px;\">\n  <span id=\"credits-left-notice\">Lade Credits...<\/span>\n  <br>\n  <span id=\"ocr-left-notice\" style=\"display:none;\">Lade OCR-Limits...<\/span>\n  <div id=\"upgrade-button-container\" style=\"display:none;\">\n    <a href=\"https:\/\/textsnapper.com\/en\/pricing\/\" class=\"upgrade-btn\">upgrade now<\/a>\n  <\/div>\n<\/div>\n\n<style>\n  #credits-left-notice, #ocr-left-notice {\n    font-family: 'Atkinson Hyperlegible', Arial, sans-serif;\n    font-size: 16px;\n    color: #555;\n  }\n\n  .warning {\n    color: #f44336;\n    font-weight: bold;\n  }\n\n  .unlimited {\n    color: #4CAF50;\n    font-weight: bold;\n  }\n\n  .upgrade-btn {\n    display: inline-block;\n    padding: 10px 20px;\n    background: #2F5591;\n    color: #FFFFFF;\n    font-weight: bold;\n    border: none;\n    border-radius: 4px;\n    text-decoration: none;\n    cursor: pointer;\n    transition: background 0.3s, transform 0.2s ease;\n    margin-top: 10px;\n  }\n\n  .upgrade-btn:hover {\n    background: #24467a;\n    transform: translateY(-2px);\n  }\n<\/style>\n\n<script>\ndocument.addEventListener('DOMContentLoaded', async function () {\n  const lang = document.documentElement.lang.startsWith('en') ? 'en' : 'de';\n  const noticeEl = document.getElementById('credits-left-notice');\n  const ocrEl = document.getElementById('ocr-left-notice');\n  const upgradeBtnContainer = document.getElementById('upgrade-button-container');\n\n  const translations = {\n    de: {\n      loading: 'Lade Credits...',\n      base_monthly: 'Monatliche Credits verf\u00fcgbar',\n      base_package: 'Paket-Credits verf\u00fcgbar (g\u00fcltig bis',\n      unlimited: 'Verf\u00fcgbare uploads: unbegrenzt \u2714\ufe0f',\n      warning: '\u26a0\ufe0f Limit fast erreicht!',\n      error: 'Fehler beim Laden.',\n      no_credits: 'Keine Credits verf\u00fcgbar.',\n      upgrade_now: 'Jetzt upgraden',\n      ocr_loading: 'Lade OCR-Limits...',\n      ocr_available: 'OCR-Uploads verf\u00fcgbar diesen Monat',\n      ocr_no_available: 'Keine OCR-Uploads mehr verf\u00fcgbar.'\n    },\n    en: {\n      loading: 'Loading credits...',\n      base_monthly: 'Monthly credits available',\n      base_package: 'Package credits available (valid until',\n      unlimited: 'Available uploads: unlimited \u2714\ufe0f',\n      warning: '\u26a0\ufe0f Limit almost reached!',\n      error: 'Error loading.',\n      no_credits: 'No credits available.',\n      upgrade_now: 'Upgrade now',\n      ocr_loading: 'Loading OCR limits...',\n      ocr_available: 'OCR uploads available this month',\n      ocr_no_available: 'No OCR uploads left.'\n    }\n  };\n\n  function t(key) {\n    return translations[lang][key];\n  }\n\n  async function fetchAjax(action) {\n    const res = await fetch(window.ajaxurl || '\/wp-admin\/admin-ajax.php', {\n      method: 'POST',\n      credentials: 'include',\n      headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\n      body: new URLSearchParams({ action })\n    });\n    return res.json();\n  }\n\n  let showUpgradeButton = false;\n\n  async function updateCreditNotice() {\n    noticeEl.classList.remove('warning', 'unlimited');\n    noticeEl.textContent = t('loading');\n\n    try {\n      const [monthlyRes, packageRes] = await Promise.all([\n        fetchAjax('get_user_credit_count'),\n        fetchAjax('get_user_package_credits')\n      ]);\n\n      if (!monthlyRes.success || !packageRes.success) {\n        noticeEl.textContent = t('error');\n        return;\n      }\n\n      const monthlyLeft = monthlyRes.data.monthly_credits_remaining;\n      const packageLeft = packageRes.data.credits;\n      const packageExpires = packageRes.data.expires;\n\n      let totalCredits = monthlyLeft + packageLeft;\n      let messages = [];\n\n      if (monthlyLeft === -1) {\n        noticeEl.textContent = t('unlimited');\n        noticeEl.classList.add('unlimited');\n        return;\n      }\n\n      messages.push(`${t('base_monthly')}: ${monthlyLeft}`);\n\n      if (packageLeft > 0) {\n        messages.push(`${t('base_package')} ${packageExpires}): ${packageLeft}`);\n      }\n\n      noticeEl.textContent = messages.join(' | ');\n\n      if (totalCredits <= 500) {\n        noticeEl.classList.add('warning');\n        noticeEl.innerHTML += `<br>${t('warning')}`;\n        showUpgradeButton = true;\n      }\n\n      if (totalCredits <= 0) {\n        noticeEl.classList.add('warning');\n        noticeEl.textContent = `${t('no_credits')}`;\n        showUpgradeButton = true;\n      }\n\n      toggleUpgradeButton();\n    } catch (e) {\n      noticeEl.textContent = t('error');\n    }\n  }\n\n  async function updateOCRNotice() {\n    ocrEl.style.display = 'inline';\n    ocrEl.textContent = t('ocr_loading');\n    ocrEl.classList.remove('warning');\n\n    try {\n      const res = await fetchAjax('get_user_ocr_count');\n      if (!res.success) {\n        ocrEl.textContent = t('error');\n        return;\n      }\n\n      const ocrLeft = res.data.ocr_limit - res.data.used_ocr;\n\n      if (res.data.ocr_limit === -1) {\n        ocrEl.textContent = t('unlimited');\n        ocrEl.classList.add('unlimited');\n        return;\n      }\n\n      if (ocrLeft <= 0) {\n        ocrEl.textContent = t('ocr_no_available');\n        ocrEl.classList.add('warning');\n        showUpgradeButton = true;\n      } else {\n        ocrEl.textContent = `${t('ocr_available')}: ${ocrLeft}`;\n        if (ocrLeft <= 1) ocrEl.classList.add('warning');\n      }\n\n      toggleUpgradeButton();\n    } catch (e) {\n      ocrEl.textContent = t('error');\n    }\n  }\n\n  function toggleUpgradeButton() {\n    upgradeBtnContainer.style.display = showUpgradeButton ? 'block' : 'none';\n  }\n\n  updateCreditNotice();\n  updateOCRNotice();\n\n  document.addEventListener('ttsUsageUpdated', updateCreditNotice);\n  document.addEventListener('ocrUsageUpdated', updateOCRNotice);\n});\n<\/script>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-173c164 e-flex e-con-boxed e-con e-parent\" data-id=\"173c164\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-39dae85 elementor-align-center elementor-widget elementor-widget-button\" data-id=\"39dae85\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"button.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<div class=\"elementor-button-wrapper\">\n\t\t\t\t\t<a class=\"elementor-button elementor-button-link elementor-size-sm\" href=\"https:\/\/textsnapper.com\/en\/library\/\">\n\t\t\t\t\t\t<span class=\"elementor-button-content-wrapper\">\n\t\t\t\t\t\t\t\t\t<span class=\"elementor-button-text\">Click here to go to My Archive \ud83d\udcda<\/span>\n\t\t\t\t\t<\/span>\n\t\t\t\t\t<\/a>\n\t\t\t\t<\/div>\n\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>","protected":false},"excerpt":{"rendered":"<p>Bild vorlesen lassen Keine Kamera verf\u00fcgbar oder Berechtigung verweigert. No camera available or permission denied. Kein Bild ausgew\u00e4hlt. No image selected. Kein Bild verf\u00fcgbar. No image available. Fehler bei der Texterkennung. Bitte nochmals klicken. Error during text recognition. Please try again. Kein Text erkannt. No text recognized. Bitte eine Stimme ausw\u00e4hlen! Please select a voice!&hellip;&nbsp;<a href=\"https:\/\/textsnapper.com\/en\/tool\/\" rel=\"bookmark\">Read More \u00bb<span class=\"screen-reader-text\">tool<\/span><\/a><\/p>","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"page-templates\/template-pagebuilder-full-width.php","meta":{"neve_meta_sidebar":"","neve_meta_container":"","neve_meta_enable_content_width":"","neve_meta_content_width":0,"neve_meta_title_alignment":"","neve_meta_author_avatar":"","neve_post_elements_order":"","neve_meta_disable_header":"","neve_meta_disable_footer":"","neve_meta_disable_title":"","footnotes":""},"class_list":["post-245","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.4 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>tool - TextSnapper<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/textsnapper.com\/en\/tool\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"tool - TextSnapper\" \/>\n<meta property=\"og:description\" content=\"Bild vorlesen lassen Keine Kamera verf\u00fcgbar oder Berechtigung verweigert. No camera available or permission denied. Kein Bild ausgew\u00e4hlt. No image selected. Kein Bild verf\u00fcgbar. No image available. Fehler bei der Texterkennung. Bitte nochmals klicken. Error during text recognition. Please try again. Kein Text erkannt. No text recognized. Bitte eine Stimme ausw\u00e4hlen! Please select a voice!&hellip;&nbsp;Read More &raquo;tool\" \/>\n<meta property=\"og:url\" content=\"https:\/\/textsnapper.com\/en\/tool\/\" \/>\n<meta property=\"og:site_name\" content=\"TextSnapper\" \/>\n<meta property=\"article:modified_time\" content=\"2025-09-09T06:16:54+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"8 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/textsnapper.com\\\/tool\\\/\",\"url\":\"https:\\\/\\\/textsnapper.com\\\/tool\\\/\",\"name\":\"tool - TextSnapper\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/textsnapper.com\\\/#website\"},\"datePublished\":\"2024-12-07T06:15:05+00:00\",\"dateModified\":\"2025-09-09T06:16:54+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/textsnapper.com\\\/tool\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/textsnapper.com\\\/tool\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/textsnapper.com\\\/tool\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/textsnapper.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"tool\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/textsnapper.com\\\/#website\",\"url\":\"https:\\\/\\\/textsnapper.com\\\/\",\"name\":\"TextSnapper\",\"description\":\"Texte aus Bildern vorlesen, \u00fcbersetzen und einfach h\u00f6ren.\",\"publisher\":{\"@id\":\"https:\\\/\\\/textsnapper.com\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/textsnapper.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/textsnapper.com\\\/#organization\",\"name\":\"Textsnapper\",\"url\":\"https:\\\/\\\/textsnapper.com\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/textsnapper.com\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/textsnapper.com\\\/wp-content\\\/uploads\\\/2024\\\/12\\\/cropped-cropped-cropped-textsnapper.png\",\"contentUrl\":\"https:\\\/\\\/textsnapper.com\\\/wp-content\\\/uploads\\\/2024\\\/12\\\/cropped-cropped-cropped-textsnapper.png\",\"width\":200,\"height\":200,\"caption\":\"Textsnapper\"},\"image\":{\"@id\":\"https:\\\/\\\/textsnapper.com\\\/#\\\/schema\\\/logo\\\/image\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"tool - TextSnapper","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/textsnapper.com\/en\/tool\/","og_locale":"en_US","og_type":"article","og_title":"tool - TextSnapper","og_description":"Bild vorlesen lassen Keine Kamera verf\u00fcgbar oder Berechtigung verweigert. No camera available or permission denied. Kein Bild ausgew\u00e4hlt. No image selected. Kein Bild verf\u00fcgbar. No image available. Fehler bei der Texterkennung. Bitte nochmals klicken. Error during text recognition. Please try again. Kein Text erkannt. No text recognized. Bitte eine Stimme ausw\u00e4hlen! Please select a voice!&hellip;&nbsp;Read More &raquo;tool","og_url":"https:\/\/textsnapper.com\/en\/tool\/","og_site_name":"TextSnapper","article_modified_time":"2025-09-09T06:16:54+00:00","twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"8 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/textsnapper.com\/tool\/","url":"https:\/\/textsnapper.com\/tool\/","name":"tool - TextSnapper","isPartOf":{"@id":"https:\/\/textsnapper.com\/#website"},"datePublished":"2024-12-07T06:15:05+00:00","dateModified":"2025-09-09T06:16:54+00:00","breadcrumb":{"@id":"https:\/\/textsnapper.com\/tool\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/textsnapper.com\/tool\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/textsnapper.com\/tool\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/textsnapper.com\/"},{"@type":"ListItem","position":2,"name":"tool"}]},{"@type":"WebSite","@id":"https:\/\/textsnapper.com\/#website","url":"https:\/\/textsnapper.com\/","name":"TextSnapper","description":"Texte aus Bildern vorlesen, \u00fcbersetzen und einfach h\u00f6ren.","publisher":{"@id":"https:\/\/textsnapper.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/textsnapper.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/textsnapper.com\/#organization","name":"Textsnapper","url":"https:\/\/textsnapper.com\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/textsnapper.com\/#\/schema\/logo\/image\/","url":"https:\/\/textsnapper.com\/wp-content\/uploads\/2024\/12\/cropped-cropped-cropped-textsnapper.png","contentUrl":"https:\/\/textsnapper.com\/wp-content\/uploads\/2024\/12\/cropped-cropped-cropped-textsnapper.png","width":200,"height":200,"caption":"Textsnapper"},"image":{"@id":"https:\/\/textsnapper.com\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/textsnapper.com\/en\/wp-json\/wp\/v2\/pages\/245","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/textsnapper.com\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/textsnapper.com\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/textsnapper.com\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/textsnapper.com\/en\/wp-json\/wp\/v2\/comments?post=245"}],"version-history":[{"count":1277,"href":"https:\/\/textsnapper.com\/en\/wp-json\/wp\/v2\/pages\/245\/revisions"}],"predecessor-version":[{"id":10729,"href":"https:\/\/textsnapper.com\/en\/wp-json\/wp\/v2\/pages\/245\/revisions\/10729"}],"wp:attachment":[{"href":"https:\/\/textsnapper.com\/en\/wp-json\/wp\/v2\/media?parent=245"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}