{"id":25439,"date":"2025-06-12T14:47:03","date_gmt":"2025-06-12T06:47:03","guid":{"rendered":"https:\/\/www.hkmu.edu.hk\/oetools\/?page_id=25439"},"modified":"2025-11-03T17:01:06","modified_gmt":"2025-11-03T09:01:06","slug":"agent-1-tts","status":"publish","type":"page","link":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-1-tts\/","title":{"rendered":"Agent 1 Podcast Creator"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"25439\" class=\"elementor elementor-25439\" data-elementor-settings=\"[]\">\n\t\t\t\t\t\t\t<div class=\"elementor-section-wrap\">\n\t\t\t\t\t\t\t<section class=\"has_eae_slider wavo-column-gap-default elementor-section elementor-top-section elementor-element elementor-element-e1106bd elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"e1106bd\" data-element_type=\"section\" data-settings=\"{&quot;jet_parallax_layout_list&quot;:[],&quot;background_background&quot;:&quot;classic&quot;}\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"has_eae_slider elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-f28283c\" data-id=\"f28283c\" data-element_type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t\t\t<div class=\"elementor-element elementor-element-fd4be8d elementor-widget__width-inherit elementor-widget elementor-widget-html\" data-id=\"fd4be8d\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n    <meta charset=\"UTF-8\">\r\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n    <title>Podcast Creator<\/title>\r\n    <script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pdf.js\/3.11.174\/pdf.min.js\"><\/script>\r\n    <script src=\"https:\/\/unpkg.com\/mammoth@1.4.8\/mammoth.browser.min.js\"><\/script>\r\n    <script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jszip\/3.10.1\/jszip.min.js\"><\/script>\r\n    <style>\r\n        \/* ================ GLOBAL STYLES ================ *\/\r\n        :root {\r\n            \/* New color palette *\/\r\n            --primary-color: #fa975e;\r\n            --primary-light: #e8864a;\r\n            --primary-dark: #d87a40;\r\n            --primary-blue: #6495ED;\r\n            --accent-color: #fa975e;\r\n            --accent-green: #7AC143;\r\n            --light-gray: #f9f5f3;\r\n            --medium-gray: #e0e0e0;\r\n            --dark-gray: #707070;\r\n            --success-color: #7AC143;\r\n            --danger-color: #f44336;\r\n            --warning-color: #ff9800;\r\n            --info-color: #fa975e;\r\n            --primary-red: #e53e3e;\r\n            --accent-orange: #ed8936;\r\n            --dark-text: #333;\r\n            --light-text: #666;\r\n            --card-bg: rgba(255, 255, 255, 0.95);\r\n            --bg-light: linear-gradient(135deg, #fa975e, #e8864a);\r\n            --shadow: 0 15px 40px rgba(0, 0, 0, 0.2);\r\n            \r\n            \/* UI elements *\/\r\n            --border-radius: 12px;\r\n            --small-radius: 8px;\r\n            --box-shadow: 0 2px 8px rgba(0,0,0,0.1);\r\n            --box-shadow-hover: 0 8px 25px rgba(0,0,0,0.15);\r\n            --transition: all 0.3s ease;\r\n        }\r\n        \r\n        \/* Reset and base styles *\/\r\n        * {\r\n            margin: 0 auto;\r\n            padding: 5 !important;\r\n            box-sizing: border-box !important;\r\n            font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif !important;\r\n        }\r\n        \r\n        body {\r\n            background: linear-gradient(135deg, #fa975e, #e8864a);\r\n            color: #333;\r\n            font-size: 1rem;\r\n            line-height: 1.6;\r\n            scroll-behavior: smooth;\r\n            min-height: 100vh;\r\n            padding: 24px;\r\n        }\r\n\r\n        \/* Main container*\/\r\n        .container {\r\n            max-width: 1600px;\r\n\t\t\tpadding: 25px;\r\n            margin: 0 auto;\r\n            background: rgba(255, 255, 255, 0.95);\r\n            border-radius: 12px;\r\n            box-shadow: 0 15px 40px rgba(0, 0, 0, 0.2);\r\n            position: relative;\r\n            margin-top: 24px;\r\n            margin-bottom: 24px;\r\n            transition: all 0.3s ease;;\r\n            animation: fadeInUp 0.6s ease-out;\r\n        }\r\n\r\n        .container:hover {\r\n            transform: translateY(-2px);\r\n            box-shadow: 0 12px 30px rgba(0,0,0,0.2);\r\n        }\r\n\r\n        \/* Section styling *\/\r\n        .section {\r\n            margin-bottom: 32px;\r\n            padding: 24px;\r\n            border-radius: 12px;\r\n            background: white;\r\n            border: 1px solid var(--medium-gray);\r\n            box-shadow: var(--box-shadow);\r\n        }\r\n\r\n        .section h2 {\r\n            color: #fa975e;\r\n            font-size: 1.5rem;\r\n            margin-bottom: 24px;\r\n            font-weight: 600;\r\n            border-bottom: 2px solid #fa975e;\r\n            padding-bottom: 8px;\r\n        }\r\n\r\n        \/* Step title styling *\/\r\n        .step-title {\r\n            color: var(--dark-text);\r\n            font-weight: 600;\r\n            margin-bottom: 16px;\r\n            display: block;\r\n            font-size: 1.1rem;\r\n        }\r\n\r\n        \/* Audio Player styling *\/\r\n        .audio-player {\r\n            background: white;\r\n        }\r\n\r\n        .audio-player audio {\r\n            width: 100%;\r\n            margin-bottom: 24px;\r\n            border-radius: var(--small-radius);\r\n        }\r\n\r\n        .flex-row {\r\n            display: flex;\r\n            gap: 16px;\r\n            flex-wrap: wrap;\r\n        }\r\n\r\n        \/* Loading message styling *\/\r\n        .loading-message {\r\n            text-align: center;\r\n            color: var(--primary-color);\r\n            font-weight: 600;\r\n            font-size: 1.3rem;\r\n            padding: 20px;\r\n            background: rgba(250, 151, 94, 0.1);\r\n            border-radius: var(--border-radius);\r\n            margin-bottom: 24px;\r\n            border: 1px solid rgba(250, 151, 94, 0.2);\r\n            animation: pulse 2s infinite;\r\n        }\r\n\r\n        @keyframes pulse {\r\n            0% { opacity: 1; }\r\n            50% { opacity: 0.7; }\r\n            100% { opacity: 1; }\r\n        }\r\n\r\n        \/* Header styling *\/\r\n        .header {\r\n            background: linear-gradient(45deg, #fa975e, #e8864a) !important;\r\n            color: #ffffff !important;\r\n            padding: 20px !important;\r\n            text-align: center !important;\r\n            border-radius: 12px 12px 0 0;\r\n            margin-bottom: 24px;\r\n        }\r\n\r\n        .header h1 {\r\n            font-size: 2.5rem !important;\r\n            font-weight: 700 !important;\r\n            margin-bottom: 10px !important;\r\n            color: #ffffff !important;\r\n            position: relative !important;\r\n            text-shadow: 0 2px 4px rgba(0,0,0,0.2) !important;\r\n        }\r\n\r\n        .header p {\r\n            font-size: 1.1rem !important;\r\n            color: #ffffff !important;\r\n            opacity: 0.9 !important;\r\n            max-width: 600px !important;\r\n            margin: 0 auto !important;\r\n            position: relative !important;\r\n        }\r\n\r\n        \/* Upload section *\/\r\n        .upload-section {\r\n            border: 2px dashed var(--primary-color);\r\n            border-radius: var(--border-radius);\r\n            padding: 10px!important;\r\n            text-align: center;\r\n            margin-top: 10px !important;\r\n            margin-bottom: 32px!important;\r\n            cursor: pointer;\r\n            background: rgba(250, 151, 94, 0.05);\r\n            transition: var(--transition);\r\n            position: relative;\r\n            overflow: hidden;\r\n        }\r\n\r\n        .upload-section::before {\r\n            content: '';\r\n            position: absolute;\r\n            top: 0;\r\n            left: -100%;\r\n            width: 100%;\r\n            height: 100%;\r\n            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);\r\n            transition: left 0.5s;\r\n        }\r\n\r\n        .upload-section:hover {\r\n            border-color: var(--primary-light);\r\n            background: rgba(232, 134, 74, 0.15);\r\n            transform: translateY(-2px);\r\n            box-shadow: var(--box-shadow);\r\n        }\r\n\r\n        .upload-section:hover::before {\r\n            left: 100%;\r\n        }\r\n\r\n        .upload-section p {\r\n            color: var(--dark-text);\r\n            font-weight: 500;\r\n            margin-bottom: 8px;\r\n        }\r\n\r\n        .upload-section p:last-child {\r\n            color: var(--light-text);\r\n            font-size: 0.9rem;\r\n        }\r\n\r\n        .text-input {\r\n            width: 100%;\r\n            min-height: 200px;\r\n            padding: 20px;\r\n            border: 2px solid var(--medium-gray);\r\n            border-radius: var(--border-radius);\r\n            font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif;\r\n            resize: vertical;\r\n            margin-top: 16px important!;\r\n            margin-bottom: 16px important!;\r\n            transition: var(--transition);\r\n            background: linear-gradient(135deg, #ffffff, #f9f9f9);\r\n            font-size: 1rem;\r\n            line-height: 1.6;\r\n        }\r\n\r\n        .text-input:focus {\r\n            outline: none;\r\n            border-color: var(--primary-color);\r\n            box-shadow: 0 0 0 3px rgba(250, 151, 94, 0.2);\r\n            background: #ffffff;\r\n        }\r\n\r\n        .char-count {\r\n            color: var(--light-text);\r\n            font-size: 0.9rem;\r\n            text-align: right;\r\n            margin-bottom: 24px;\r\n            font-weight: 500;\r\n        }\r\n\r\n        \/* Button *\/\r\n        .button {\r\n            background: #7AC143;\r\n            color: white;\r\n            border: none;\r\n            padding: 8px 28px;\r\n            border-radius: var(--border-radius);\r\n            cursor: pointer;\r\n            margin-right: 16px;\r\n            margin-bottom: 16px;\r\n            font-weight: 600;\r\n            font-size: 24px;\r\n            transition: var(--transition);\r\n            box-shadow: 0 4px 8px rgba(250, 151, 94, 0.3);\r\n            position: relative;\r\n            overflow: hidden;\r\n        }\r\n\r\n        .button::before {\r\n            content: '';\r\n            position: absolute;\r\n            top: 50%;\r\n            left: 50%;\r\n            width: 0;\r\n            height: 0;\r\n            background: rgba(255,255,255,0.2);\r\n            border-radius: 50%;\r\n            transform: translate(-50%, -50%);\r\n            transition: width 0.3s, height 0.3s;\r\n        }\r\n\r\n        .button:hover {\r\n            background: linear-gradient(135deg, #5a8f32, #5a8f32);\r\n            transform: translateY(-2px);\r\n            box-shadow: 0 6px 12px rgba(250, 151, 94, 0.4);\r\n        }\r\n\r\n        .button:hover::before {\r\n            width: 300px;\r\n            height: 300px;\r\n        }\r\n\r\n        .button:active {\r\n            transform: translateY(0);\r\n        }\r\n\r\n        .button:disabled {\r\n            background: linear-gradient(135deg, var(--medium-gray), #bbb);\r\n            cursor: not-allowed;\r\n            transform: none;\r\n            box-shadow: none;\r\n        }\r\n\r\n        .button:disabled:hover {\r\n            transform: none;\r\n        }\r\n\r\n        \/* Success button variant *\/\r\n        .button.success {\r\n            background: linear-gradient(135deg, var(--success-color), #5a9a2b);\r\n        }\r\n\r\n        .button.success:hover {\r\n            background: linear-gradient(135deg, #5a9a2b, var(--success-color));\r\n            box-shadow: 0 6px 12px rgba(122, 193, 67, 0.4);\r\n        }\r\n\r\n        \/* File info improvements *\/\r\n        .file-info {\r\n            background: linear-gradient(135deg, rgba(122, 193, 67, 0.1), rgba(122, 193, 67, 0.05));\r\n            padding: 24px;\r\n            border-radius: var(--border-radius);\r\n            margin-bottom: 16px;\r\n            border: 1px solid rgba(122, 193, 67, 0.2);\r\n            transition: var(--transition);\r\n        }\r\n\r\n        .file-info:hover {\r\n            background: linear-gradient(135deg, rgba(122, 193, 67, 0.15), rgba(122, 193, 67, 0.1));\r\n            transform: translateY(-1px);\r\n        }\r\n\r\n        .file-info p {\r\n            color: var(--dark-text);\r\n            font-weight: 500;\r\n            margin-bottom: 16px;\r\n        }\r\n\r\n        \/* Progress bar *\/\r\n        .pdf-progress {\r\n            margin-top: 16px;\r\n            font-size: 0.9rem;\r\n            color: var(--dark-text);\r\n        }\r\n\r\n        .progress-bar {\r\n            width: 100%;\r\n            height: 8px;\r\n            background: var(--light-gray);\r\n            margin-top: 8px;\r\n            border-radius: var(--small-radius);\r\n            overflow: hidden;\r\n            box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);\r\n        }\r\n\r\n        .progress-fill {\r\n            height: 100%;\r\n            background: linear-gradient(90deg, var(--primary-color), var(--primary-light));\r\n            width: 0%;\r\n            transition: width 0.3s ease;\r\n            border-radius: var(--small-radius);\r\n            position: relative;\r\n        }\r\n\r\n\t\t.progress-fill::after {\r\n\t\t\tcontent: '';\r\n\t\t\tposition: absolute;\r\n\t\t\ttop: 0;\r\n\t\t\tleft: 0;\r\n\t\t\twidth: 100%;\r\n\t\t\theight: 100%;\r\n\t\t\tbackground: linear-gradient(\r\n\t\t\t\t90deg, \r\n\t\t\t\trgba(255,255,255,0) 0%,\r\n\t\t\t\trgba(255,255,255,0.3) 50%,\r\n\t\t\t\trgba(255,255,255,0) 100%\r\n\t\t\t);\r\n\t\t\tanimation: shimmer 1.5s infinite;\r\n\t\t}\r\n\r\n\t\t@keyframes shimmer {\r\n\t\t\t0% { background-position: -150% 0; }\r\n\t\t\t100% { background-position: 150% 0; }\r\n\t\t}\r\n\r\n        \/* Document stats styling *\/\r\n        .document-stats {\r\n            display: flex;\r\n            justify-content: space-around;\r\n            margin: 20px 0;\r\n            padding: 15px;\r\n            background: rgba(250, 151, 94, 0.1);\r\n            border-radius: var(--border-radius);\r\n            border: 1px solid rgba(250, 151, 94, 0.2);\r\n        }\r\n\r\n        .stat-item {\r\n            text-align: center;\r\n            padding: 10px;\r\n        }\r\n\r\n        .stat-number {\r\n            font-size: 1.8rem;\r\n            font-weight: 700;\r\n            color: var(--primary-color);\r\n        }\r\n\r\n        .stat-label {\r\n            font-size: 0.9rem;\r\n            color: var(--dark-gray);\r\n            text-transform: uppercase;\r\n            letter-spacing: 1px;\r\n        }\r\n\r\n        \/* Logging system improvements - Made invisible *\/\r\n        .log-container {\r\n            position: fixed;\r\n            bottom: 24px;\r\n            right: 24px;\r\n            width: 0;\r\n            height: 0;\r\n            opacity: 0;\r\n            visibility: hidden;\r\n            z-index: -1;\r\n        }\r\n\r\n        .log-header {\r\n            display: none;\r\n        }\r\n\r\n        .log-content {\r\n            display: none;\r\n        }\r\n\r\n        .log-entry {\r\n            display: none;\r\n        }\r\n\r\n        \/* Hidden state *\/\r\n        .hidden {\r\n            display: none !important;\r\n        }\r\n\r\n        \/* Label improvements *\/\r\n        label {\r\n            color: var(--dark-text);\r\n            font-weight: 600;\r\n            margin-bottom: 8px;\r\n            display: block;\r\n            font-size: 1.1rem;\r\n        }\r\n\r\n        \/* Animation for page load *\/\r\n        @keyframes fadeInUp {\r\n            from {\r\n                opacity: 0;\r\n                transform: translateY(30px);\r\n            }\r\n            to {\r\n                opacity: 1;\r\n                transform: translateY(0);\r\n            }\r\n        }\r\n\r\n        \/* Mobile responsiveness *\/\r\n        @media (max-width: 768px) {\r\n            body {\r\n                padding: 16px;\r\n            }\r\n\r\n            .container {\r\n                padding: 32px;\r\n                margin-top: 16px;\r\n                margin-bottom: 16px;\r\n            }\r\n\r\n            .header h1 {\r\n                font-size: 1.8rem;\r\n            }\r\n\r\n            .upload-section {\r\n                padding: 32px;\r\n            }\r\n\r\n            .text-input {\r\n                min-height: 150px;\r\n                padding: 16px;\r\n            }\r\n\r\n            .button {\r\n                padding: 16px 24px;\r\n                font-size: 0.9rem;\r\n                margin-right: 8px;\r\n                margin-bottom: 8px;\r\n            }\r\n\r\n            .flex-row {\r\n                flex-direction: column;\r\n            }\r\n\r\n            .flex-row .button {\r\n                width: 100%;\r\n                margin-right: 0;\r\n            }\r\n            \r\n            .document-stats {\r\n                flex-direction: column;\r\n                gap: 15px;\r\n            }\r\n        }\r\n\r\n        @media (max-width: 480px) {\r\n            .container {\r\n                padding: 24px;\r\n            }\r\n\r\n            .header h1 {\r\n                font-size: 1.5rem;\r\n            }\r\n\r\n            .button {\r\n                width: 100%;\r\n                margin-right: 0;\r\n                margin-bottom: 16px;\r\n            }\r\n        }\r\n\r\n        \/* Focus styles for accessibility *\/\r\n        *:focus {\r\n            outline: 2px solid var(--primary-color);\r\n            outline-offset: 2px;\r\n        }\r\n\r\n        \/* Smooth scrolling *\/\r\n        @media (prefers-reduced-motion: no-preference) {\r\n            html {\r\n                scroll-behavior: smooth;\r\n            }\r\n        }\r\n    <\/style>\r\n<\/head>\r\n<body>\r\n    <div class=\"container\">\r\n        <!-- New header with subtitle -->\r\n        <div class=\"header\">\r\n            <h1>Intelligent Podcast Creator<\/h1>\r\n            <p>Transform any text or document into a polished, five\u2011minute English podcast hosted by virtual presenters\u2014ideal for flipped classrooms and revision<\/p>\r\n        <\/div>\r\n        \r\n        <div class=\"upload-section\" id=\"uploadSection\">\r\n            <div id=\"uploadPrompt\">\r\n                <p>Click to upload or drag and drop a document<\/p>\r\n                <p>Supported formats: TXT, SRT, JSON, XML, HTML, MD, CSV, ASS, SSA, DOCX, PPTX, PDF<\/p>\r\n                <p>Max 1000 words will be extracted<\/p>\r\n            <\/div>\r\n            <div id=\"fileInfo\" class=\"file-info hidden\">\r\n                <p id=\"fileName\">No file selected<\/p>\r\n                <button class=\"button\" onclick=\"clearFile()\">Clear<\/button>\r\n            <\/div>\r\n            <div id=\"pdfProgress\" class=\"pdf-progress hidden\">\r\n                <div id=\"pdfProgressText\">Processing document...<\/div>\r\n                <div class=\"progress-bar\">\r\n                    <div class=\"progress-fill\" id=\"progressFill\"><\/div>\r\n                <\/div>\r\n            <\/div>\r\n            \r\n            <!-- Document Statistics Display -->\r\n            <div id=\"documentStats\" class=\"document-stats hidden\">\r\n                <div class=\"stat-item\">\r\n                    <div class=\"stat-number\" id=\"pageCount\">0<\/div>\r\n                    <div class=\"stat-label\">Pages<\/div>\r\n                <\/div>\r\n                <div class=\"stat-item\">\r\n                    <div class=\"stat-number\" id=\"wordCount\">0<\/div>\r\n                    <div class=\"stat-label\">Words<\/div>\r\n                <\/div>\r\n                <div class=\"stat-item\">\r\n                    <div class=\"stat-number\" id=\"charCount\">0<\/div>\r\n                    <div class=\"stat-label\">Characters<\/div>\r\n                <\/div>\r\n            <\/div>\r\n        <\/div>\r\n        \r\n        <input type=\"file\" id=\"fileInput\" style=\"display: none;\" \r\n               accept=\".txt,.srt,.json,.xml,.html,.md,.csv,.ass,.ssa,.docx,.pptx,.pdf\">\r\n        \r\n        <!-- Text Input Section -->\r\n        <div class=\"section\" id=\"textInputSection\">\r\n            <h2>Input Content<\/h2>\r\n            <label for=\"textInput\" class=\"step-title\">Enter text content (max 1000 words):<\/label>\r\n            <textarea id=\"textInput\" class=\"text-input\" placeholder=\"Enter text here or upload document...\"><\/textarea>\r\n            <div class=\"flex-row\">\r\n                <button class=\"button\" onclick=\"sendToCozeAPI()\" id=\"sendButton\">Generate Podcast<\/button>\r\n                <button class=\"button\" onclick=\"clearAllContent()\">Clear All<\/button>\r\n            <\/div>\r\n        <\/div>\r\n\r\n        <!-- Audio Player Section -->\r\n        <div class=\"section\" id=\"audioSection\">\r\n            <h2>Generated Podcast<\/h2>\r\n            <div id=\"loadingMessage\" class=\"loading-message hidden\">\r\n                Generating Audio Clips, Please Wait...\r\n            <\/div>\r\n            <div class=\"audio-player\">\r\n                <audio id=\"mainPlayer\" controls><\/audio>\r\n                <div class=\"flex-row\">\r\n                    <button class=\"button\" id=\"playBtn\" disabled>Play<\/button>\r\n                    <button class=\"button\" id=\"pauseBtn\" disabled>Pause<\/button>\r\n                    <button class=\"button\" id=\"downloadBtn\" disabled>Download<\/button>\r\n                    <button class=\"button\" id=\"copyUrlBtn\" disabled>Copy URL<\/button>\r\n                <\/div>\r\n            <\/div>\r\n        <\/div>\r\n    <\/div>\r\n\r\n    <!-- Logging container (hidden) -->\r\n    <div class=\"log-container\">\r\n        <div class=\"log-header\" id=\"logHeader\">API Logs<\/div>\r\n        <div class=\"log-content\" id=\"logContent\"><\/div>\r\n    <\/div>\r\n\r\n    <script>\r\n        \/\/ =====================================\r\n        \/\/ BACKEND AUTHORISATION SYSTEM\r\n        \/\/ =====================================\r\n        function modInverse(k1, mod = 256) {\r\n            let t = 0, newT = 1, r = mod, newR = k1;\r\n            while (newR !== 0) {\r\n                const quotient = Math.floor(r \/ newR);\r\n                [t, newT] = [newT, t - quotient * newT];\r\n                [r, newR] = [newR, r - quotient * newR];\r\n            }\r\n            if (r > 1) throw new Error('k1 is not invertible');\r\n            if (t < 0) t += mod;\r\n            return t;\r\n        }\r\n\r\n        function decryptServerPayload(payload) {\r\n            if (payload.length < 12) throw new Error('Invalid payload: too short');\r\n            const suffix = payload.slice(-12), b64 = payload.slice(0, -12);\r\n            const numericKey = BigInt(suffix.slice(0, 10));\r\n            const k1 = Number((numericKey % 127n) * 2n + 1n), k2 = Number(numericKey % 256n);\r\n            const invK1 = modInverse(k1, 256);\r\n            const binaryString = atob(b64);\r\n            const bytes = new Uint8Array(binaryString.length);\r\n            for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);\r\n            const decryptedBytes = new Uint8Array(bytes.length);\r\n            for (let i = 0; i < bytes.length; i++) {\r\n                const temp = (bytes[i] - k2 + 256) % 256;\r\n                decryptedBytes[i] = (invK1 * temp) % 256;\r\n            }\r\n            return new TextDecoder('utf-8').decode(decryptedBytes);\r\n        }\r\n\r\n        async function fetchDecryptedKey(service) {\r\n            const sanitizedBase = BACKEND_API_URL.endsWith('\/') ? BACKEND_API_URL.slice(0, -1) : BACKEND_API_URL;\r\n            const url = `${sanitizedBase}\/get_encrypted_key?service=${service}`;\r\n            const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application\/json' } });\r\n            if (!response.ok) throw new Error(`Failed to fetch ${service} key: ${response.status}`);\r\n            const data = await response.json();\r\n            if (!data.encrypted) throw new Error(`No encrypted key returned for ${service}`);\r\n            return decryptServerPayload(data.encrypted);\r\n        }\r\n\r\n        async function initializeApiKeys() {\r\n            return { cozeKey: await fetchDecryptedKey('coze') };\r\n        }\r\n\r\n        function clearApiKeys() {\r\n            if (typeof COZE_API_KEY_RUNTIME !== 'undefined') COZE_API_KEY_RUNTIME = null;\r\n        }\r\n\r\n        async function retryWithKeyRefresh(apiCallFn, maxRetries = 3) {\r\n            let lastError;\r\n            for (let attempt = 1; attempt <= maxRetries; attempt++) {\r\n                try {\r\n                    return await apiCallFn();\r\n                } catch (error) {\r\n                    lastError = error;\r\n                    const is401or403 = error.message.includes('401') || error.message.includes('403');\r\n                    if (is401or403 && attempt < maxRetries) {\r\n                        COZE_API_KEY_RUNTIME = await fetchDecryptedKey('coze');\r\n                    } else {\r\n                        throw lastError;\r\n                    }\r\n                }\r\n            }\r\n            throw lastError;\r\n        }\r\n\r\n        \/\/ =====================================\r\n        \/\/ API Configuration\r\n        \/\/ =====================================\r\n        let COZE_API_KEY_RUNTIME = null;\r\n        Object.defineProperty(window, 'COZE_API_KEY', {\r\n            get() { return COZE_API_KEY_RUNTIME; },\r\n            configurable: false\r\n        });\r\n\r\n        const BACKEND_API_URL = (() => {\r\n            try {\r\n                const { protocol, hostname } = window.location;\r\n                if (protocol === 'https:' && (hostname === 'oetools.net' || hostname.endsWith('.oetools.net'))) {\r\n                    return '\/api';\r\n                }\r\n            } catch (error) {}\r\n            return 'https:\/\/oetools.net\/api';\r\n        })();\r\n\r\n        const COZE_API_URL = 'https:\/\/api.coze.com\/open_api\/v2\/chat';\r\n        const COZE_BOT_ID = '7523230787173367815';\r\n        const USER_ID = 'text_to_audio_user_' + Date.now();\r\n        const MAX_WORDS = 1000;\r\n\r\n        \/\/ Supported file types\r\n        const supportedTypes = ['txt', 'srt', 'json', 'xml', 'html', 'md', 'csv', 'ass', 'ssa', 'docx', 'pptx', 'pdf'];\r\n\r\n        let selectedFile = null;\r\n        let currentAudioUrl = null;\r\n\r\n        \/\/ Initialize word counter\r\n        const textInput = document.getElementById('textInput');\r\n        const charCountDisplay = document.getElementById('charCountDisplay');\r\n        \r\n        textInput.addEventListener('input', updateWordCount);\r\n        \r\n        function updateWordCount() {\r\n            const text = textInput.value.trim();\r\n            const wordCount = countWords(text);\r\n            \r\n            \/\/ Enforce word limit\r\n            if (wordCount > MAX_WORDS) {\r\n                const words = text.split(\/\\s+\/);\r\n                const limitedWords = words.slice(0, MAX_WORDS);\r\n                textInput.value = limitedWords.join(' ');\r\n                updateWordCount();\r\n            }\r\n        }\r\n        \r\n        function countWords(text) {\r\n            if (!text.trim()) return 0;\r\n            \/\/ Count both Chinese characters and English words\r\n            const chineseChars = text.match(\/[\\u4e00-\\u9fa5]\/g) || [];\r\n            const englishWords = text.replace(\/[\\u4e00-\\u9fa5]\/g, '').match(\/\\S+\/g) || [];\r\n            return chineseChars.length + englishWords.length;\r\n        }\r\n\r\n        \/\/ File upload handling\r\n        const fileInput = document.getElementById('fileInput');\r\n        const uploadSection = document.getElementById('uploadSection');\r\n        const fileInfo = document.getElementById('fileInfo');\r\n\r\n        fileInput.addEventListener('change', handleFileSelect);\r\n        uploadSection.addEventListener('click', handleUploadClick);\r\n        uploadSection.addEventListener('dragover', handleDragOver);\r\n        uploadSection.addEventListener('drop', handleDrop);\r\n        uploadSection.addEventListener('dragleave', handleDragLeave);\r\n\r\n        function handleUploadClick(event) {\r\n            if (!event.target.closest('#fileInfo') && !event.target.closest('#pdfProgress')) {\r\n                fileInput.click();\r\n            }\r\n        }\r\n\r\n        function handleFileSelect(event) {\r\n            const file = event.target.files[0];\r\n            if (file) {\r\n                processSelectedFile(file);\r\n            }\r\n        }\r\n\r\n        function handleDragOver(event) {\r\n            event.preventDefault();\r\n        }\r\n\r\n        function handleDragLeave(event) {\r\n            event.preventDefault();\r\n        }\r\n\r\n        function handleDrop(event) {\r\n            event.preventDefault();\r\n            const file = event.dataTransfer.files[0];\r\n            if (file) {\r\n                processSelectedFile(file);\r\n            }\r\n        }\r\n\r\n        function clearFile() {\r\n            selectedFile = null;\r\n            fileInput.value = '';\r\n            fileInfo.classList.add('hidden');\r\n            document.getElementById('pdfProgress').classList.add('hidden');\r\n            document.getElementById('documentStats').classList.add('hidden');\r\n            document.getElementById('uploadPrompt').style.display = 'block';\r\n            logApiCall('INFO', 'File cleared');\r\n        }\r\n\r\n        function clearAllContent() {\r\n            clearFile();\r\n            textInput.value = '';\r\n            updateWordCount();\r\n            hideAudioPlayer();\r\n            logApiCall('INFO', 'All content cleared');\r\n        }\r\n\r\n        \/\/ =====================================\r\n        \/\/ File Parsing Functions\r\n        \/\/ =====================================\r\n        \r\n        \/**\r\n         * Get file extension from filename\r\n         * @param {string} filename - The filename\r\n         * @returns {string} The file extension\r\n         *\/\r\n        function getFileExtension(filename) {\r\n            return filename.split('.').pop().toLowerCase();\r\n        }\r\n        \r\n        \/**\r\n         * Extract text from DOCX files\r\n         * @param {File} file - The DOCX file\r\n         * @returns {Promise<string>} The extracted text\r\n         *\/\r\n        async function extractDocxText(file) {\r\n            try {\r\n                const arrayBuffer = await file.arrayBuffer();\r\n                const result = await mammoth.extractRawText({ arrayBuffer });\r\n                return result.value;\r\n            } catch (error) {\r\n                throw new Error('Failed to read DOCX file: ' + error.message);\r\n            }\r\n        }\r\n        \r\n        \/**\r\n         * Extract text from PPTX files\r\n         * @param {File} file - The PPTX file\r\n         * @returns {Promise<string>} The extracted text\r\n         *\/\r\n        async function extractPptxText(file) {\r\n            try {\r\n                const arrayBuffer = await file.arrayBuffer();\r\n                const zip = await JSZip.loadAsync(arrayBuffer);\r\n                let text = '';\r\n                \r\n                \/\/ Extract text from slides\r\n                const slideFiles = Object.keys(zip.files).filter(name => \r\n                    name.startsWith('ppt\/slides\/slide') && name.endsWith('.xml')\r\n                );\r\n                \r\n                for (const slideFile of slideFiles) {\r\n                    const content = await zip.file(slideFile).async('string');\r\n                    \/\/ Simple XML parsing to extract text\r\n                    const textMatches = content.match(\/<a:t[^>]*>([^<]*)<\\\/a:t>\/g);\r\n                    if (textMatches) {\r\n                        textMatches.forEach(match => {\r\n                            const textContent = match.replace(\/<[^>]*>\/g, '');\r\n                            if (textContent.trim()) {\r\n                                text += textContent + '\\n';\r\n                            }\r\n                        });\r\n                    }\r\n                }\r\n                \r\n                return text;\r\n            } catch (error) {\r\n                throw new Error('Failed to read PPTX file: ' + error.message);\r\n            }\r\n        }\r\n        \r\n        \/**\r\n         * Extract text from PDF files\r\n         * @param {File} file - The PDF file\r\n         * @returns {Promise<string>} The extracted text\r\n         *\/\r\n        async function extractPdfText(file) {\r\n            try {\r\n                const arrayBuffer = await file.arrayBuffer();\r\n                const pdf = await pdfjsLib.getDocument(arrayBuffer).promise;\r\n                \r\n                \/\/ Update page count\r\n                document.getElementById('pageCount').textContent = pdf.numPages;\r\n                \r\n                let fullText = '';\r\n                for (let i = 1; i <= pdf.numPages; i++) {\r\n                    const page = await pdf.getPage(i);\r\n                    const textContent = await page.getTextContent();\r\n                    fullText += textContent.items.map(item => item.str).join(' ') + '\\n\\n';\r\n                    updatePDFProgress(`Processing page ${i}\/${pdf.numPages}...`, 30 + (i \/ pdf.numPages * 50));\r\n                }\r\n                \r\n                return fullText;\r\n            } catch (error) {\r\n                throw new Error('Failed to read PDF file: ' + error.message);\r\n            }\r\n        }\r\n        \r\n        \/**\r\n         * Read text from text-based files\r\n         * @param {File} file - The text file\r\n         * @returns {Promise<string>} The file content\r\n         *\/\r\n        function readTextFile(file) {\r\n            return new Promise((resolve, reject) => {\r\n                const reader = new FileReader();\r\n                reader.onload = (e) => resolve(e.target.result);\r\n                reader.onerror = (e) => reject(new Error('Failed to read file'));\r\n                reader.readAsText(file);\r\n            });\r\n        }\r\n        \r\n        \/**\r\n         * Process file based on its extension\r\n         * @param {File} file - The file to process\r\n         *\/\r\n        async function extractTextFromFile(file) {\r\n            try {\r\n                const extension = getFileExtension(file.name);\r\n                let content = '';\r\n                \r\n                \/\/ Handle different file types\r\n                if (extension === 'pdf') {\r\n                    content = await extractPdfText(file);\r\n                } else if (extension === 'docx') {\r\n                    content = await extractDocxText(file);\r\n                } else if (extension === 'pptx') {\r\n                    content = await extractPptxText(file);\r\n                } else {\r\n                    \/\/ For text-based files\r\n                    content = await readTextFile(file);\r\n                }\r\n                \r\n                return content;\r\n            } catch (error) {\r\n                throw error;\r\n            }\r\n        }\r\n        \r\n        \/**\r\n         * Update document statistics display\r\n         * @param {string} content - The extracted content\r\n         *\/\r\n        function updateDocumentStats(content) {\r\n            \/\/ Calculate stats\r\n            const words = content.trim().split(\/\\s+\/).filter(word => word.length > 0);\r\n            const wordCount = words.length;\r\n            const charCount = content.length;\r\n            \r\n            \/\/ Update UI\r\n            document.getElementById('wordCount').textContent = wordCount.toLocaleString();\r\n            document.getElementById('charCount').textContent = charCount.toLocaleString();\r\n            \r\n            \/\/ For non-PDF files, set page count to 1\r\n            if (!selectedFile || getFileExtension(selectedFile.name) !== 'pdf') {\r\n                document.getElementById('pageCount').textContent = '1';\r\n            }\r\n            \r\n            \/\/ Show stats container\r\n            document.getElementById('documentStats').classList.remove('hidden');\r\n        }\r\n\r\n        async function processSelectedFile(file) {\r\n            const extension = getFileExtension(file.name);\r\n            \r\n            if (!supportedTypes.includes(extension)) {\r\n                logApiCall('ERROR', `Unsupported file type: ${extension}`);\r\n                return;\r\n            }\r\n\r\n            if (file.size > 50 * 1024 * 1024) {\r\n                logApiCall('ERROR', 'File size must be less than 50MB');\r\n                return;\r\n            }\r\n\r\n            selectedFile = file;\r\n            \r\n            \/\/ Update file info display\r\n            document.getElementById('fileName').textContent = file.name;\r\n            document.getElementById('uploadPrompt').style.display = 'none';\r\n            fileInfo.classList.remove('hidden');\r\n            \r\n            \/\/ Process file\r\n            logApiCall('INFO', `Starting ${extension.toUpperCase()} processing`);\r\n            \r\n            try {\r\n                document.getElementById('pdfProgress').classList.remove('hidden');\r\n                updatePDFProgress(`Processing ${extension.toUpperCase()}...`, 10);\r\n                \r\n                const content = await extractTextFromFile(file);\r\n                updatePDFProgress('Extracting text...', 70);\r\n                \r\n                \/\/ Apply word limit (1000 words)\r\n                const words = content.trim().split(\/\\s+\/).filter(word => word.length > 0);\r\n                let limitedContent = '';\r\n                let wordCount = 0;\r\n                \r\n                for (const word of words) {\r\n                    if (wordCount < MAX_WORDS) {\r\n                        limitedContent += word + ' ';\r\n                        wordCount++;\r\n                    } else {\r\n                        break;\r\n                    }\r\n                }\r\n                \r\n                textInput.value = limitedContent.trim();\r\n                updateWordCount();\r\n                updateDocumentStats(limitedContent);\r\n                updatePDFProgress('Document processing complete', 100);\r\n                logApiCall('SUCCESS', `Extracted ${wordCount} words from document`);\r\n                \r\n                setTimeout(() => {\r\n                    document.getElementById('pdfProgress').classList.add('hidden');\r\n                }, 2000);\r\n                \r\n            } catch (error) {\r\n                console.error('Processing document:', error);\r\n                updatePDFProgress('Processing document', 100);\r\n                logApiCall('ERROR', `Document processing failed: ${error.message}`);\r\n                \r\n                setTimeout(() => {\r\n                    document.getElementById('pdfProgress').classList.add('hidden');\r\n                }, 2000);\r\n            }\r\n        }\r\n\r\n        function updatePDFProgress(text, percentage) {\r\n            document.getElementById('pdfProgressText').textContent = text;\r\n            document.getElementById('progressFill').style.width = percentage + '%';\r\n        }\r\n\r\n        \/\/ Coze output parsing function\r\n        function parseCozeResponse(responseData) {\r\n            try {\r\n                if (responseData && responseData.messages) {\r\n                    for (const message of responseData.messages) {\r\n                        if (message.role === 'assistant' && message.type === 'tool_response') {\r\n                            try {\r\n                                const content = JSON.parse(message.content);\r\n                                if (content.output && content.output.includes('.mp3')) {\r\n                                    return content.output;\r\n                                }\r\n                            } catch (e) {\r\n                                \/\/ Continue searching if this content isn't valid JSON\r\n                                continue;\r\n                            }\r\n                        }\r\n                    }\r\n                }\r\n                return null;\r\n            } catch (error) {\r\n                logApiCall('ERROR', `Error parsing Coze response: ${error.message}`);\r\n                return null;\r\n            }\r\n        }\r\n\r\n        \/\/ Audio player functions\r\n        function showAudioPlayer(audioUrl) {\r\n            currentAudioUrl = audioUrl;\r\n            const mainPlayer = document.getElementById('mainPlayer');\r\n            const loadingMessage = document.getElementById('loadingMessage');\r\n            \r\n            \/\/ Hide loading message\r\n            loadingMessage.classList.add('hidden');\r\n            \r\n            mainPlayer.src = audioUrl;\r\n            \r\n            \/\/ Enable audio player buttons\r\n            document.getElementById('playBtn').disabled = false;\r\n            document.getElementById('pauseBtn').disabled = false;\r\n            document.getElementById('downloadBtn').disabled = false;\r\n            document.getElementById('copyUrlBtn').disabled = false;\r\n            \r\n            \/\/ Scroll to audio player\r\n            document.getElementById('audioSection').scrollIntoView({ behavior: 'smooth', block: 'center' });\r\n            \r\n            logApiCall('SUCCESS', `Audio player loaded with URL: ${audioUrl}`);\r\n        }\r\n\r\n        function hideAudioPlayer() {\r\n            const mainPlayer = document.getElementById('mainPlayer');\r\n            const loadingMessage = document.getElementById('loadingMessage');\r\n            \r\n            \/\/ Hide loading message and reset player\r\n            loadingMessage.classList.add('hidden');\r\n            mainPlayer.src = '';\r\n            currentAudioUrl = null;\r\n            \r\n            \/\/ Disable audio player buttons\r\n            document.getElementById('playBtn').disabled = true;\r\n            document.getElementById('pauseBtn').disabled = true;\r\n            document.getElementById('downloadBtn').disabled = true;\r\n            document.getElementById('copyUrlBtn').disabled = true;\r\n        }\r\n\r\n        function showLoadingMessage() {\r\n            const loadingMessage = document.getElementById('loadingMessage');\r\n            loadingMessage.classList.remove('hidden');\r\n        }\r\n\r\n        \/\/ Audio player event listeners\r\n        document.getElementById('playBtn').addEventListener('click', () => {\r\n            const player = document.getElementById('mainPlayer');\r\n            player.play();\r\n        });\r\n\r\n        document.getElementById('pauseBtn').addEventListener('click', () => {\r\n            const player = document.getElementById('mainPlayer');\r\n            player.pause();\r\n        });\r\n\r\n        document.getElementById('downloadBtn').addEventListener('click', () => {\r\n            if (currentAudioUrl) {\r\n                const a = document.createElement('a');\r\n                a.href = currentAudioUrl;\r\n                a.download = `podcast_${Date.now()}.mp3`;\r\n                document.body.appendChild(a);\r\n                a.click();\r\n                document.body.removeChild(a);\r\n                logApiCall('INFO', 'Audio download initiated');\r\n            }\r\n        });\r\n\r\n        document.getElementById('copyUrlBtn').addEventListener('click', () => {\r\n            if (currentAudioUrl) {\r\n                navigator.clipboard.writeText(currentAudioUrl).then(() => {\r\n                    logApiCall('SUCCESS', 'Audio URL copied to clipboard');\r\n                    \r\n                    \/\/ Visual feedback\r\n                    const btn = document.getElementById('copyUrlBtn');\r\n                    const originalText = btn.textContent;\r\n                    btn.textContent = 'Copied!';\r\n                    setTimeout(() => {\r\n                        btn.textContent = originalText;\r\n                    }, 2000);\r\n                }).catch(err => {\r\n                    logApiCall('ERROR', `Failed to copy URL: ${err.message}`);\r\n                });\r\n            }\r\n        });\r\n\r\n        async function sendToCozeAPI() {\r\n            const textContent = textInput.value.trim();\r\n            \r\n            if (!textContent) {\r\n                logApiCall('ERROR', 'No text content to send');\r\n                return;\r\n            }\r\n\r\n            const sendButton = document.getElementById('sendButton');\r\n            sendButton.disabled = true;\r\n            sendButton.textContent = 'Generating...';\r\n            \r\n            \/\/ Show loading message and hide any previous audio\r\n            hideAudioPlayer();\r\n            showLoadingMessage();\r\n            \r\n            logApiCall('INFO', 'Sending to Coze API');\r\n\r\n            try {\r\n                const requestData = {\r\n                    conversation_id: Date.now().toString(),\r\n                    bot_id: COZE_BOT_ID,\r\n                    user: USER_ID,\r\n                    query: textContent,\r\n                    stream: false\r\n                };\r\n\r\n                logApiCall('REQUEST', {\r\n                    url: COZE_API_URL,\r\n                    headers: {\r\n                        'Authorization': `Bearer ${COZE_API_KEY ? COZE_API_KEY.substring(0, 10) + '...' : 'N\/A'}`,\r\n                        'Content-Type': 'application\/json'\r\n                    },\r\n                    body: requestData,\r\n                    textLength: textContent.length,\r\n                    textPreview: textContent.substring(0, 100) + (textContent.length > 100 ? '...' : '')\r\n                });\r\n\r\n                const performCozeCall = async () => {\r\n                    const response = await fetch(COZE_API_URL, {\r\n                        method: 'POST',\r\n                        headers: {\r\n                            'Authorization': `Bearer ${COZE_API_KEY}`,\r\n                            'Content-Type': 'application\/json'\r\n                        },\r\n                        body: JSON.stringify(requestData)\r\n                    });\r\n\r\n                    const responseData = await response.json();\r\n                    \r\n                    if (!response.ok) {\r\n                        logApiCall('ERROR', {\r\n                            status: response.status,\r\n                            error: responseData\r\n                        });\r\n                        throw new Error(`HTTP error ${response.status}: ${JSON.stringify(responseData)}`);\r\n                    }\r\n\r\n                    return { response, responseData };\r\n                };\r\n\r\n                const { response, responseData } = await retryWithKeyRefresh(performCozeCall);\r\n                \r\n                if (response.ok) {\r\n                    logApiCall('RESPONSE', {\r\n                        status: response.status,\r\n                        data: responseData\r\n                    });\r\n\r\n                    \/\/ Parse the response to extract MP3 URL\r\n                    const audioUrl = parseCozeResponse(responseData);\r\n                    if (audioUrl) {\r\n                        logApiCall('SUCCESS', `MP3 URL extracted: ${audioUrl}`);\r\n                        showAudioPlayer(audioUrl);\r\n                        sendButton.textContent = 'Generate New Podcast';\r\n                    } else {\r\n                        logApiCall('WARNING', 'No MP3 URL found in response');\r\n                        hideAudioPlayer();\r\n                        sendButton.textContent = 'Generate Podcast';\r\n                    }\r\n                } else {\r\n                    logApiCall('ERROR', {\r\n                        status: response.status,\r\n                        message: response.statusText,\r\n                        data: responseData\r\n                    });\r\n                    hideAudioPlayer();\r\n                    sendButton.textContent = 'Generate Podcast';\r\n                }\r\n\r\n            } catch (error) {\r\n                console.error('Error sending to Coze API:', error);\r\n                logApiCall('ERROR', {\r\n                    name: error.name,\r\n                    message: error.message,\r\n                    stack: error.stack\r\n                });\r\n                hideAudioPlayer();\r\n                sendButton.textContent = 'Generate Podcast';\r\n            } finally {\r\n                sendButton.disabled = false;\r\n            }\r\n        }\r\n\r\n        \/\/ ===================\r\n        \/\/ Logging Functions\r\n        \/\/ ===================\r\n        function logApiCall(type, data) {\r\n            \/\/ Logging is now invisible, but kept for functionality\r\n            console.log(`${type}:`, data);\r\n        }\r\n        \r\n        \/\/ Initialize page\r\n        document.addEventListener('DOMContentLoaded', async function() {\r\n            \/\/ Disable audio buttons initially\r\n            hideAudioPlayer();\r\n            \r\n            \/\/ Fetch API keys from backend\r\n            try {\r\n                console.log('Initializing API keys from backend...');\r\n                const { cozeKey } = await initializeApiKeys();\r\n                COZE_API_KEY_RUNTIME = cozeKey;\r\n                console.log('\u2713 Coze API key initialized successfully');\r\n            } catch (error) {\r\n                console.error('Failed to initialize API keys:', error);\r\n                alert('Authorisation was not granted by the server. This may be due to a network issue. Please check your connection and try again.');\r\n            }\r\n        });\r\n\r\n        window.addEventListener('beforeunload', clearApiKeys);\r\n    <\/script>\r\n<\/body>\r\n<\/html>\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Podcast Creator Intelligent Podcast Creator Transform any text or document into a polished, five\u2011minute English podcast hosted by virtual presenters\u2014ideal for flipped classrooms and revision Click to upload or drag and drop a document Supported formats: TXT, SRT, JSON, XML, HTML, MD, CSV, ASS, SSA, DOCX, PPTX, PDF Max 1000 words will be extracted No...<\/p>\n","protected":false},"author":748,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"redux-templates_canvas","meta":{"_expiration-date-status":"","_expiration-date":0,"_expiration-date-type":"","_expiration-date-categories":[],"_expiration-date-options":[]},"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v17.3 (Yoast SEO v21.2) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Agent 1 Podcast Creator - Hong Kong Metropolitan University<\/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:\/\/www.hkmu.edu.hk\/oetools\/agent-1-tts\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Agent 1 Podcast Creator\" \/>\n<meta property=\"og:description\" content=\"Podcast Creator Intelligent Podcast Creator Transform any text or document into a polished, five\u2011minute English podcast hosted by virtual presenters\u2014ideal\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.hkmu.edu.hk\/oetools\/agent-1-tts\/\" \/>\n<meta property=\"og:site_name\" content=\"Hong Kong Metropolitan University\" \/>\n<meta property=\"article:modified_time\" content=\"2025-11-03T09:01:06+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=\"1 minute\" \/>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Agent 1 Podcast Creator - Hong Kong Metropolitan University","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:\/\/www.hkmu.edu.hk\/oetools\/agent-1-tts\/","og_locale":"en_US","og_type":"article","og_title":"Agent 1 Podcast Creator","og_description":"Podcast Creator Intelligent Podcast Creator Transform any text or document into a polished, five\u2011minute English podcast hosted by virtual presenters\u2014ideal","og_url":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-1-tts\/","og_site_name":"Hong Kong Metropolitan University","article_modified_time":"2025-11-03T09:01:06+00:00","twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"1 minute"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-1-tts\/","url":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-1-tts\/","name":"Agent 1 Podcast Creator - Hong Kong Metropolitan University","isPartOf":{"@id":"https:\/\/www.hkmu.edu.hk\/oetools\/#website"},"datePublished":"2025-06-12T06:47:03+00:00","dateModified":"2025-11-03T09:01:06+00:00","breadcrumb":{"@id":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-1-tts\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.hkmu.edu.hk\/oetools\/agent-1-tts\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-1-tts\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Open Educational Tools","item":"\/oetools\/"},{"@type":"ListItem","position":2,"name":"Agent 1 Podcast Creator"}]},{"@type":"WebSite","@id":"https:\/\/www.hkmu.edu.hk\/oetools\/#website","url":"https:\/\/www.hkmu.edu.hk\/oetools\/","name":"Hong Kong Metropolitan University","description":"Open Educational Tools - Hong Kong Metropolitan University","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.hkmu.edu.hk\/oetools\/?s={search_term_string}"},"query-input":"required name=search_term_string"}],"inLanguage":"en-US"}]}},"_links":{"self":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages\/25439"}],"collection":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/users\/748"}],"replies":[{"embeddable":true,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/comments?post=25439"}],"version-history":[{"count":166,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages\/25439\/revisions"}],"predecessor-version":[{"id":30903,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages\/25439\/revisions\/30903"}],"wp:attachment":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/media?parent=25439"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}