{"id":25667,"date":"2025-06-13T16:28:16","date_gmt":"2025-06-13T08:28:16","guid":{"rendered":"https:\/\/www.hkmu.edu.hk\/oetools\/?page_id=25667"},"modified":"2026-02-20T15:44:01","modified_gmt":"2026-02-20T07:44:01","slug":"agent-3-podcast","status":"publish","type":"page","link":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-3-podcast\/","title":{"rendered":"Agent 3 Conversation Creator"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"25667\" class=\"elementor elementor-25667\" 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-a490f1a elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"a490f1a\" data-element_type=\"section\" data-settings=\"{&quot;jet_parallax_layout_list&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-75e9909\" data-id=\"75e9909\" data-element_type=\"column\" data-settings=\"{&quot;background_background&quot;:&quot;classic&quot;}\">\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-c850842 elementor-widget__width-inherit elementor-widget elementor-widget-html\" data-id=\"c850842\" 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\t<meta charset=\"UTF-8\">\r\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n\t<title>Dynamic Conversation Builder<\/title>\r\n\t<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pdf.js\/3.11.174\/pdf.min.js\"><\/script>\r\n\t<script src=\"https:\/\/unpkg.com\/mammoth@1.4.8\/mammoth.browser.min.js\"><\/script>\r\n\t<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jszip\/3.10.1\/jszip.min.js\"><\/script>\r\n\t<style>\r\n\t\t\/* ================ GLOBAL STYLES ================ *\/\r\n\t\t:root {\r\n\t\t\t--primary-color: #fa975e; --primary-light: #e8864a; --primary-dark: #d87a40;\r\n\t\t\t--accent-teal: #fa975e; --accent-green: #7AC143; --light-gray: #f9f5f3;\r\n\t\t\t--medium-gray: #e0e0e0; --dark-gray: #707070; --success-color: #7AC143;\r\n\t\t\t--danger-color: #f44336; --warning-color: #ff9800; --info-color: #fa975e;\r\n\t\t\t--dark-text: #333; --light-text: #666; --card-bg: #ffffff;\r\n\t\t\t--bg-light: linear-gradient(135deg, #fa975e, #e8864a);\r\n\t\t\t--shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\r\n\t\t\t--border-radius: 12px; --small-radius: 8px;\r\n\t\t\t--box-shadow: 0 2px 8px rgba(0,0,0,0.1);\r\n\t\t\t--box-shadow-hover: 0 8px 25px rgba(0,0,0,0.15);\r\n\t\t\t--transition: all 0.3s ease;\r\n\t\t}\r\n\r\n\t\t* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif; }\r\n\t\tbody { background: var(--bg-light); color: var(--dark-text); font-size: 1rem; line-height: 1.6; min-height: 100vh; }\r\n\t\t.container { max-width: 1800px; margin: 0 auto; padding: 0px; background: rgba(255, 255, 255, 0.95); border-radius: var(--border-radius); box-shadow: var(--shadow); overflow: hidden; }\r\n\t\t.header { background: linear-gradient(45deg, #fa975e, #e8864a); color: #ffffff; padding: 20px; text-align: center; }\r\n\t\t.header h1 { font-size: 2.5rem; font-weight: 700; margin-bottom: 10px; color: #ffffff !important; position: relative; text-shadow: 0 2px 4px rgba(0,0,0,0.2); }\r\n\t\t.header p { font-size: 1.1rem; color: #ffffff; opacity: 0.9; max-width: 600px; margin: 0 auto; position: relative; }\r\n\t\th2 { font-size: 1.5rem; color: var(--primary-color); margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid var(--primary-color); font-weight: 600; display: flex; align-items: center; gap: 10px; }\r\n\t\th3 { font-size: 1.3rem; color: var(--primary-color); margin-bottom: 16px; font-weight: 600; }\r\n\t\th4 { font-size: 1.2rem; color: var(--primary-color); margin-bottom: 8px; font-weight: 600; }\r\n\r\n\t\t\/* Document Statistics *\/\r\n\t\t.document-stats { background-color: #f8f9fa; padding: 15px; border-radius: 8px; border: 1px solid #e0e6ed; }\r\n\t\t.stat-item { text-align: center; }\r\n\t\t.stat-number { font-size: 1.5rem; font-weight: 600; color: var(--primary-color); }\r\n\t\t.stat-label { font-size: 0.8rem; color: var(--light-text); margin-top: 5px; }\r\n\r\n\t\t\/* File Info Styles - Matching Agent 4, 5, 6 *\/\r\n\t\t.file-info { \r\n\t\t\tbackground: #f8f9fa; \r\n\t\t\tborder-radius: 8px; \r\n\t\t\tpadding: 15px; \r\n\t\t\tmargin: 15px 0; \r\n\t\t\tborder-left: 4px solid #fa975e; \r\n\t\t\tdisplay: none; \r\n\t\t}\r\n\t\t\r\n\t\t.file-info h4 {\r\n\t\t\tcolor: #fa975e;\r\n\t\t\tmargin-bottom: 10px;\r\n\t\t\tfont-size: 1.1rem;\r\n\t\t\tfont-weight: 600;\r\n\t\t}\r\n\t\t\r\n\t\t.file-details {\r\n\t\t\tbackground: #ffffff;\r\n\t\t\tpadding: 15px;\r\n\t\t\tborder-radius: 8px;\r\n\t\t\tmargin-bottom: 15px;\r\n\t\t\tbox-shadow: 0 2px 4px rgba(0,0,0,0.05);\r\n\t\t\tdisplay: flex;\r\n\t\t\tflex-direction: row;\r\n\t\t\tflex-wrap: wrap;\r\n\t\t\tgap: 20px 30px;\r\n\t\t\talign-items: center;\r\n\t\t}\r\n\t\t\r\n\t\t.file-detail-row {\r\n\t\t\tdisplay: flex;\r\n\t\t\tflex-direction: row;\r\n\t\t\talign-items: center;\r\n\t\t\tgap: 8px;\r\n\t\t\tmargin-bottom: 0;\r\n\t\t\tflex-shrink: 0;\r\n\t\t}\r\n\t\t\r\n\t\t.file-detail-row.hidden {\r\n\t\t\tdisplay: none;\r\n\t\t}\r\n\t\t\r\n\t\t.file-detail-label {\r\n\t\t\tfont-weight: 600;\r\n\t\t\tcolor: #666;\r\n\t\t\tfont-size: 14px;\r\n\t\t\twhite-space: nowrap;\r\n\t\t}\r\n\t\t\r\n\t\t.file-detail-value {\r\n\t\t\tcolor: #333;\r\n\t\t\tfont-size: 14px;\r\n\t\t\twhite-space: nowrap;\r\n\t\t}\r\n\r\n\t\t\/* Sections *\/\r\n\t\t.section { background-color: var(--card-bg); padding: 32px; margin-bottom: 24px; border-radius: var(--border-radius); box-shadow: var(--box-shadow); transition: var(--transition); }\r\n\t\t.section:hover { box-shadow: var(--box-shadow-hover); transform: translateY(-2px); }\r\n\r\n\t\t\/* Form Elements *\/\r\n\t\t.drop-area { border: 2px dashed var(--primary-color); padding: 32px; text-align: center; cursor: pointer; border-radius: var(--small-radius); margin-bottom: 16px; background: rgba(250, 151, 94, 0.05); transition: var(--transition); }\r\n\t\t.drop-area:hover, .drop-area.active { border-color: var(--primary-light); background: rgba(232, 134, 74, 0.15); }\r\n\t\ttextarea, input, select { width: 100%; padding: 16px; border: 1px solid var(--medium-gray); border-radius: var(--small-radius); font-size: 1rem; transition: var(--transition); margin-bottom: 8px; }\r\n\t\ttextarea { min-height: 150px; resize: vertical; }\r\n\t\ttextarea:focus, input:focus, select:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(250, 151, 94, 0.2); }\r\n\r\n\t\t\/* Buttons *\/\r\n\t\tbutton { background: var(--accent-green); color: white; border: none; padding: 12px 28px; border-radius: var(--small-radius); cursor: pointer; font-weight: 600; font-size: 1rem; transition: var(--transition); box-shadow: 0 4px 8px rgba(122, 193, 67, 0.3); }\r\n\t\tbutton:hover { background: #5a8f32; transform: translateY(-2px); box-shadow: 0 6px 12px rgba(122, 193, 67, 0.4); }\r\n\t\tbutton:disabled { background: var(--medium-gray); cursor: not-allowed; transform: none; box-shadow: none; }\r\n\t\t.add-speaker-btn { background: var(--accent-green); }\r\n\t\t.add-speaker-btn:hover { background: #5a8f32; }\r\n\t\t.remove-speaker-btn { background: var(--danger-color); margin-top: 16px; }\r\n\t\t.remove-speaker-btn:hover { background: #c53030; }\r\n\r\n\t\t\/* Speaker Controls *\/\r\n\t\t.speaker-controls { display: flex; align-items: center; gap: 16px; margin: 24px 0; }\r\n\t\t.speaker-cards { display: flex; gap: 24px; flex-wrap: wrap; }\r\n\t\t.speaker-card { border: 1px solid var(--light-gray); padding: 24px; border-radius: var(--small-radius); background: var(--light-gray); min-width: 200px; flex: 1; transition: var(--transition); }\r\n\t\t.speaker-card:hover { transform: translateY(-3px); box-shadow: var(--box-shadow); }\r\n\t\t.control-group { margin-bottom: 16px; }\r\n\t\t.control-group label { display: block; font-weight: 600; margin-bottom: 8px; color: var(--dark-gray); }\r\n\r\n\t\t\/* Audio Player *\/\r\n\t\t.audio-player { margin: 24px 0; background: var(--light-gray); padding: 24px; border-radius: var(--small-radius); }\r\n\t\t.audio-player audio { width: 100%; margin: 16px 0; }\r\n\t\t.audio-player-controls { display: flex; gap: 16px; margin-top: 16px; }\r\n\r\n\t\t\/* Script Cards *\/\r\n\t\t.script-card { margin: 16px 0; padding: 24px; border: 1px solid var(--light-gray); border-radius: var(--small-radius); background: var(--light-gray); }\r\n\r\n\r\n\t\t\/* Progress Bar *\/\r\n\t\t.progress-container { margin: 10px 0; height: 6px; background: #e0e0e0; border-radius: 3px; overflow: hidden; display: none; }\r\n\t\t.progress-bar { height: 100%; background: var(--primary-color); width: 0%; transition: width 0.3s ease; }\r\n\r\n\r\n\t\t\/* Utility Classes *\/\r\n\t\t.reminder { color: var(--dark-gray); font-size: 0.9rem; margin-top: 8px; }\r\n\t\t.warning { color: var(--danger-color); font-weight: bold; padding: 16px; background: #ffe0e0; border-radius: var(--small-radius); margin: 16px 0; display: none; }\r\n\t\t.flex-row { display: flex; gap: 16px; margin-top: 16px; }\r\n\r\n\t\t\/* Responsive *\/\r\n\t\t@media (max-width: 768px) {\r\n\t\t\t.speaker-cards { flex-direction: column; }\r\n\t\t\t.flex-row { flex-direction: column; }\r\n\t\t\t.file-details {flex-direction: column;align-items: flex-start;gap: 12px;}\r\n\t\t\t.file-detail-row {width: 100%;flex-direction: row;justify-content: space-between;}\r\n\t\t\t.file-detail-label {margin-bottom: 0;}\r\n\t\t}\r\n\t\t\r\n\t\t@media (max-width: 480px) {\r\n\t\t\t.file-details {gap: 10px 15px;}\r\n\t\t\t.file-detail-row {flex-wrap: wrap;}\r\n\t\t}\r\n\t<\/style>\r\n<\/head>\r\n<body>\r\n\t<div class=\"container\">\r\n\t\t<!-- Header -->\r\n\t\t<div class=\"header\">\r\n\t\t\t<h1>Dynamic Conversation Builder<\/h1>\r\n\t\t\t<p>Extract keywords, create summaries, and build interactive conversations from your documents<\/p>\r\n\t\t<\/div>\r\n\r\n\t\t<!-- Step 1: Input Content -->\r\n\t\t<div class=\"section\">\r\n\t\t\t<h2>Step 1: Input Content<\/h2>\r\n\t\t\t<div class=\"drop-area\" id=\"dropArea\">\r\n\t\t\t\t<p>Drag & drop a document here or click to select<\/p>\r\n\t\t\t\t<p style=\"font-size: 0.9rem; margin-top: 10px; color: #666;\">\r\n\t\t\t\t\tSupported formats: TXT, SRT, JSON, XML, HTML, MD, CSV, ASS, SSA, DOCX, PPTX, PDF\r\n\t\t\t\t<\/p>\r\n\t\t\t<\/div>\r\n\t\t\t<input type=\"file\" id=\"fileInput\" style=\"display: none;\" accept=\".txt,.srt,.json,.xml,.html,.md,.csv,.ass,.ssa,.docx,.pptx,.pdf\">\r\n\t\t\t\r\n\t\t\t<div class=\"progress-container\" id=\"progressContainer\">\r\n\t\t\t\t<div class=\"progress-bar\" id=\"progressBar\"><\/div>\r\n\t\t\t<\/div>\r\n\t\t\t\r\n\t\t\t<!-- File Information Display -->\r\n\t\t\t<div class=\"file-info\" id=\"fileInfo\">\r\n\t\t\t\t<h4>Document Information<\/h4>\r\n\t\t\t\t\r\n\t\t\t\t<!-- File Details -->\r\n\t\t\t\t<div class=\"file-details\">\r\n\t\t\t\t\t<div class=\"file-detail-row\">\r\n\t\t\t\t\t\t<div class=\"file-detail-label\">File Name:<\/div>\r\n\t\t\t\t\t\t<div class=\"file-detail-value\" id=\"fileNameDisplay\">-<\/div>\r\n\t\t\t\t\t<\/div>\r\n\t\t\t\t\t<div class=\"file-detail-row\">\r\n\t\t\t\t\t\t<div class=\"file-detail-label\">File Type:<\/div>\r\n\t\t\t\t\t\t<div class=\"file-detail-value\" id=\"fileTypeDisplay\">-<\/div>\r\n\t\t\t\t\t<\/div>\r\n\t\t\t\t\t<div class=\"file-detail-row\">\r\n\t\t\t\t\t\t<div class=\"file-detail-label\">File Size:<\/div>\r\n\t\t\t\t\t\t<div class=\"file-detail-value\" id=\"fileSizeDisplay\">-<\/div>\r\n\t\t\t\t\t<\/div>\r\n\t\t\t\t\t<div class=\"file-detail-row\">\r\n\t\t\t\t\t\t<div class=\"file-detail-label\">Pages:<\/div>\r\n\t\t\t\t\t\t<div class=\"file-detail-value\" id=\"pageCount\">-<\/div>\r\n\t\t\t\t\t<\/div>\r\n\t\t\t\t\t<div class=\"file-detail-row\">\r\n\t\t\t\t\t\t<div class=\"file-detail-label\">Words:<\/div>\r\n\t\t\t\t\t\t<div class=\"file-detail-value\" id=\"wordCount\">-<\/div>\r\n\t\t\t\t\t<\/div>\r\n\t\t\t\t\t<div class=\"file-detail-row\">\r\n\t\t\t\t\t\t<div class=\"file-detail-label\">Characters:<\/div>\r\n\t\t\t\t\t\t<div class=\"file-detail-value\" id=\"charCount\">-<\/div>\r\n\t\t\t\t\t<\/div>\r\n\t\t\t\t<\/div>\r\n\t\t\t<\/div>\r\n\t\t\t\r\n\t\t\t<textarea id=\"userInput\" placeholder=\"Enter your text here or drop a document above...\" maxlength=\"12000\"><\/textarea>\r\n\t\t\t<div class=\"reminder\">Maximum 2000 Words. Please consider chunking longer content for better results.<\/div>\r\n\t\t<\/div>\r\n\r\n\t\t<!-- Step 2: Audio Configuration -->\r\n\t\t<div class=\"section\">\r\n\t\t\t<h2>Step 2: Audio Configuration<\/h2>\r\n\t\t\t<div class=\"warning\" id=\"configWarning\">Please configure speakers before generating scripts<\/div>\r\n\t\t\t\r\n\t\t\t<div class=\"speaker-controls\">\r\n\t\t\t\t<button class=\"add-speaker-btn\" onclick=\"addSpeaker()\">Add Speaker<\/button>\r\n\t\t\t\t<span id=\"speakerCount\">1 Speaker<\/span>\r\n\t\t\t<\/div>\r\n\t\t\t\r\n\t\t\t<div class=\"speaker-cards\" id=\"speakerCards\">\r\n\t\t\t\t<div class=\"speaker-card\" data-speaker=\"1\">\r\n\t\t\t\t\t<h4>Speaker 1<\/h4>\r\n\t\t\t\t\t<div class=\"control-group\">\r\n\t\t\t\t\t\t<label>Name:<\/label>\r\n\t\t\t\t\t\t<input type=\"text\" class=\"speaker-name\" value=\"Bob\" placeholder=\"Speaker Name\">\r\n\t\t\t\t\t<\/div>\r\n\t\t\t\t\t<div class=\"control-group\">\r\n\t\t\t\t\t\t<label>Voice:<\/label>\r\n\t\t\t\t\t\t<select class=\"voice-id\"><\/select>\r\n\t\t\t\t\t<\/div>\r\n\t\t\t\t\t<div class=\"control-group\">\r\n\t\t\t\t\t\t<label>Language:<\/label>\r\n\t\t\t\t\t\t<select class=\"language\"><\/select>\r\n\t\t\t\t\t<\/div>\r\n\t\t\t\t<\/div>\r\n\t\t\t<\/div>\r\n\t\t\t\r\n\t\t\t<div class=\"speaker-controls\">\r\n\t\t\t\t<button id=\"generateScriptBtn\">Generate Script<\/button>\r\n\t\t\t<\/div>\r\n\t\t<\/div>\r\n\r\n\t\t<!-- Step 3: Script Generation -->\r\n\t\t<div class=\"section\">\r\n\t\t\t<h2>Step 3: Script Generation<\/h2>\r\n\t\t\t<h3>Generated Scripts may take about 1 minute<\/h3>\r\n\t\t\t<textarea id=\"generatedScripts\" placeholder=\"Generated scripts will appear here...\"><\/textarea>\r\n\t\t\t<button id=\"generateAudioBtn\">Generate Audio<\/button>\r\n\t\t<\/div>\r\n\r\n\t\t<!-- Audio Player Section -->\r\n\t\t<div class=\"section\">\r\n\t\t\t<h2>Audio Player<\/h2>\r\n\t\t\t<div class=\"audio-player\">\r\n\t\t\t\t<h4>Complete Conversation<\/h4>\r\n\t\t\t\t<audio id=\"mainPlayer\" controls><\/audio>\r\n\t\t\t\t<div class=\"flex-row\">\r\n\t\t\t\t\t<button id=\"playAllBtn\">Play<\/button>\r\n\t\t\t\t\t<button id=\"pauseBtn\">Pause<\/button>\r\n\t\t\t\t\t<button id=\"downloadMergedBtn\" style=\"display:none;\">Download<\/button>\r\n\t\t\t\t<\/div>\r\n\t\t\t<\/div>\r\n\t\t\t\r\n\t\t\t<h3>Speaker Scripts:<\/h3>\r\n\t\t\t<div id=\"scriptCards\"><\/div>\r\n\t\t<\/div>\r\n\r\n\t<\/div>\r\n\r\n\t<script>\r\n\t\t\/\/ =====================================\r\n\t\t\/\/ BACKEND AUTHORISATION SYSTEM\r\n\t\t\/\/ =====================================\r\n\t\tfunction modInverse(k1, mod = 256) {\r\n\t\t\tlet t = 0, newT = 1;\r\n\t\t\tlet r = mod, newR = k1;\r\n\t\t\t\r\n\t\t\twhile (newR !== 0) {\r\n\t\t\t\tconst quotient = Math.floor(r \/ newR);\r\n\t\t\t\t[t, newT] = [newT, t - quotient * newT];\r\n\t\t\t\t[r, newR] = [newR, r - quotient * newR];\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tif (r > 1) throw new Error('k1 is not invertible');\r\n\t\t\tif (t < 0) t += mod;\r\n\t\t\treturn t;\r\n\t\t}\r\n\r\n\tfunction decryptServerPayload(payload) {\r\n\t\tif (payload.length < 12) {\r\n\t\t\tthrow new Error('Invalid payload: too short');\r\n\t\t}\r\n\t\tconst suffix = payload.slice(-12);\r\n\t\tconst numericKeyStr = suffix.slice(0, 10);\r\n\t\tconst b64 = payload.slice(0, -12);\r\n\t\tconst numericKey = BigInt(numericKeyStr);\r\n\t\tconst k1 = Number((numericKey % 127n) * 2n + 1n);\r\n\t\tconst k2 = Number(numericKey % 256n);\r\n\t\tconst invK1 = modInverse(k1, 256);\r\n\t\tconst binaryString = atob(b64);\r\n\t\tconst bytes = new Uint8Array(binaryString.length);\r\n\t\tfor (let i = 0; i < binaryString.length; i++) {\r\n\t\t\tbytes[i] = binaryString.charCodeAt(i);\r\n\t\t}\r\n\t\tconst decryptedBytes = new Uint8Array(bytes.length);\r\n\t\tfor (let i = 0; i < bytes.length; i++) {\r\n\t\t\tconst c = bytes[i];\r\n\t\t\tconst temp = (c - k2 + 256) % 256;\r\n\t\t\tdecryptedBytes[i] = (invK1 * temp) % 256;\r\n\t\t}\r\n\t\tconst decoder = new TextDecoder('utf-8');\r\n\t\treturn decoder.decode(decryptedBytes);\r\n\t}\r\n\r\n\tasync function fetchDecryptedKey(service) {\r\n\t\t\tconst sanitizedBase = API_BASE_URL.endsWith('\/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;\r\n\t\t\tconst url = `${sanitizedBase}\/get_encrypted_key?service=${service}`;\r\n\t\t\t\r\n\t\t\ttry {\r\n\t\t\t\tconst response = await fetch(url, {\r\n\t\t\t\t\tmethod: 'GET',\r\n\t\t\t\t\theaders: {\r\n\t\t\t\t\t\t'Accept': 'application\/json'\r\n\t\t\t\t\t}\r\n\t\t\t\t});\r\n\t\t\t\t\r\n\t\t\t\tif (!response.ok) {\r\n\t\t\t\t\tconst errorText = await response.text();\r\n\t\t\t\t\tthrow new Error(`Failed to fetch ${service} key: ${response.status} ${errorText}`);\r\n\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\t\tconst data = await response.json();\r\n\t\t\t\t\r\n\t\t\t\tif (!data.encrypted) {\r\n\t\t\t\t\tthrow new Error(`No encrypted key returned for ${service}`);\r\n\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\tconst decryptedKey = decryptServerPayload(data.encrypted);\r\n\t\t\treturn decryptedKey;\r\n\t\t\t\t\r\n\t\t\t} catch (error) {\r\n\t\t\t\tconsole.error(`Error fetching ${service} key:`, error);\r\n\t\t\t\tthrow error;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\tasync function initializeApiKeys() {\r\n\t\t\ttry {\r\n\t\t\t\tconst [cozeKey, minimaxKey] = await Promise.all([\r\n\t\t\t\t\tfetchDecryptedKey('coze'),\r\n\t\t\t\t\tfetchDecryptedKey('minimax')\r\n\t\t\t\t]);\r\n\t\t\t\t\r\n\t\t\t\treturn { cozeKey, minimaxKey };\r\n\t\t\t} catch (error) {\r\n\t\t\t\tconsole.error('Failed to initialize API keys:', error);\r\n\t\t\t\tthrow error;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\tfunction clearApiKeys() {\r\n\t\t\tif (typeof COZE_API_KEY_RUNTIME !== 'undefined') {\r\n\t\t\t\tCOZE_API_KEY_RUNTIME = null;\r\n\t\t\t}\r\n\t\t\tif (typeof MINIMAX_API_KEY_RUNTIME !== 'undefined') {\r\n\t\t\t\tMINIMAX_API_KEY_RUNTIME = null;\r\n\t\t\t}\r\n\t\t\tconsole.log('API keys cleared from memory');\r\n\t\t}\r\n\r\n\tasync function retryWithKeyRefresh(apiCallFn, serviceType, maxRetries = 3) {\r\n\t\t\tlet lastError;\r\n\t\t\t\r\n\t\t\tfor (let attempt = 1; attempt <= maxRetries; attempt++) {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tconst result = await apiCallFn();\r\n\t\t\t\t\treturn result;\r\n\t\t\t\t} catch (error) {\r\n\t\t\t\t\tlastError = error;\r\n\t\t\t\t\t\r\n\t\t\t\t\t\/\/ Check if error is 401\/403 (unauthorized\/forbidden)\r\n\t\t\t\t\tconst is401or403 = error.message.includes('401') || \r\n\t\t\t\t\t                   error.message.includes('403') ||\r\n\t\t\t\t\t                   error.message.includes('HTTP 401') ||\r\n\t\t\t\t\t                   error.message.includes('HTTP 403');\r\n\t\t\t\t\t\r\n\t\t\t\t\tif (is401or403 && attempt < maxRetries) {\r\n\t\t\t\t\t\tconsole.warn(`API call failed with auth error (attempt ${attempt}\/${maxRetries}), refreshing ${serviceType} key...`);\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\t\/\/ Refresh the specific key\r\n\t\t\t\t\t\ttry {\r\n\t\t\t\t\t\t\tconst newKey = await fetchDecryptedKey(serviceType);\r\n\t\t\t\t\t\t\tif (serviceType === 'coze') {\r\n\t\t\t\t\t\t\t\tCOZE_API_KEY_RUNTIME = newKey;\r\n\t\t\t\t\t\t\t} else if (serviceType === 'minimax') {\r\n\t\t\t\t\t\t\t\tMINIMAX_API_KEY_RUNTIME = newKey;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\tconsole.log(`\u2713 ${serviceType} key refreshed, retrying...`);\r\n\t\t\t\t\t\t} catch (refreshError) {\r\n\t\t\t\t\t\t\tconsole.error(`Failed to refresh ${serviceType} key:`, refreshError);\r\n\t\t\t\t\t\t\tthrow refreshError;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\/\/ Not an auth error or max retries reached\r\n\t\t\t\t\t\tthrow lastError;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tthrow lastError;\r\n\t\t}\r\n\r\n\t\t\/\/ =====================================\r\n\t\t\/\/ API Configuration\r\n\t\t\/\/ =====================================\r\n\t\tlet COZE_API_KEY_RUNTIME = null;\r\n\t\tlet MINIMAX_API_KEY_RUNTIME = null;\r\n\r\n\t\t\/\/ API key getters (maintains compatibility with existing code)\r\n\t\tObject.defineProperty(window, 'COZE_API_KEY', {\r\n\t\t\tget() { return COZE_API_KEY_RUNTIME; },\r\n\t\t\tconfigurable: false\r\n\t\t});\r\n\r\n\t\tObject.defineProperty(window, 'MINIMAX_API_KEY', {\r\n\t\t\tget() { return MINIMAX_API_KEY_RUNTIME; },\r\n\t\t\tconfigurable: false\r\n\t\t});\r\n\r\n\t\tconst COZE_API_URL = 'https:\/\/api.coze.com\/open_api\/v2\/chat';\r\n\t\tconst COZE_BOT_ID = '7521968709507235848';\r\n\t\tconst USER_ID = 'coze_user_' + Date.now();\r\n\t\tconst API_BASE_URL = (() => {\r\n\t\t\ttry {\r\n\t\t\t\tconst { protocol, hostname } = window.location;\r\n\t\t\t\tif (\r\n\t\t\t\t\tprotocol === 'https:' &&\r\n\t\t\t\t\t(hostname === 'oetools.net' || hostname.endsWith('.oetools.net'))\r\n\t\t\t\t) {\r\n\t\t\t\t\treturn '\/api';\r\n\t\t\t\t}\r\n\t\t\t} catch (error) {\r\n\t\t\t\tconsole.warn('API base detection fallback to public endpoint:', error);\r\n\t\t\t}\r\n\t\t\treturn 'https:\/\/oetools.net\/api';\r\n\t\t})();\r\n\t\t\r\n\t\tconst MINIMAX_API_URL = 'https:\/\/oetools.net\/minimax\/tts';\r\n\t\tconst MINIMAX_GROUP_ID = '1892726332706529707';\r\n\t\t\r\n\t\tconst VOICE_IDS = [\"Wise_Woman\", \"Abbess\", \"Calm_Woman\", \"Exuberant_Girl\", \"Inspirational_girl\", \"Lively_Girl\", \"Lovely_Girl\", \"Sweet_Girl_2\", \"Friendly_Person\", \"Imposing_Manner\", \"Casual_Guy\", \"Decent_Boy\", \"Deep_Voice_Man\", \"Determined_Man\", \"Elegant_Man\", \"Patient_Man\", \"Young_Knight\"];\r\n\t\t\r\n\t\tconst LANGUAGES = [\r\n\t\t\t{code: \"Cantonese\", name: \"Cantonese\", minimax: \"Chinese,Yue\"},\r\n\t\t\t{code: \"Chinese\", name: \"Chinese (Mandarin)\", minimax: \"Chinese\"},\r\n\t\t\t{code: \"English\", name: \"English\", minimax: \"English\"},\r\n\t\t\t{code: \"Arabic\", name: \"Arabic\", minimax: \"Arabic\"},\r\n\t\t\t{code: \"Russian\", name: \"Russian\", minimax: \"Russian\"},\r\n\t\t\t{code: \"Spanish\", name: \"Spanish\", minimax: \"Spanish\"},\r\n\t\t\t{code: \"French\", name: \"French\", minimax: \"French\"},\r\n\t\t\t{code: \"Portuguese\", name: \"Portuguese\", minimax: \"Portuguese\"},\r\n\t\t\t{code: \"German\", name: \"German\", minimax: \"German\"},\r\n\t\t\t{code: \"Turkish\", name: \"Turkish\", minimax: \"Turkish\"},\r\n\t\t\t{code: \"Dutch\", name: \"Dutch\", minimax: \"Dutch\"},\r\n\t\t\t{code: \"Ukrainian\", name: \"Ukrainian\", minimax: \"Ukrainian\"},\r\n\t\t\t{code: \"Vietnamese\", name: \"Vietnamese\", minimax: \"Vietnamese\"},\r\n\t\t\t{code: \"Indonesian\", name: \"Indonesian\", minimax: \"Indonesian\"},\r\n\t\t\t{code: \"Japanese\", name: \"Japanese\", minimax: \"Japanese\"},\r\n\t\t\t{code: \"Italian\", name: \"Italian\", minimax: \"Italian\"},\r\n\t\t\t{code: \"Korean\", name: \"Korean\", minimax: \"Korean\"},\r\n\t\t\t{code: \"Thai\", name: \"Thai\", minimax: \"Thai\"},\r\n\t\t\t{code: \"Polish\", name: \"Polish\", minimax: \"Polish\"},\r\n\t\t\t{code: \"Romanian\", name: \"Romanian\", minimax: \"Romanian\"},\r\n\t\t\t{code: \"Greek\", name: \"Greek\", minimax: \"Greek\"},\r\n\t\t\t{code: \"Czech\", name: \"Czech\", minimax: \"Czech\"},\r\n\t\t\t{code: \"Finnish\", name: \"Finnish\", minimax: \"Finnish\"},\r\n\t\t\t{code: \"Hindi\", name: \"Hindi\", minimax: \"Hindi\"}\r\n\t\t];\r\n\t\t\r\n\t\t\/\/ DOM Elements\r\n\t\tconst dropArea = document.getElementById('dropArea');\r\n\t\tconst fileInput = document.getElementById('fileInput');\r\n\t\tconst userInput = document.getElementById('userInput');\r\n\t\tconst fileInfo = document.getElementById('fileInfo');\r\n\t\tconst fileNameDisplay = document.getElementById('fileNameDisplay');\r\n\t\tconst fileTypeDisplay = document.getElementById('fileTypeDisplay');\r\n\t\tconst fileSizeDisplay = document.getElementById('fileSizeDisplay');\r\n\t\tconst progressContainer = document.getElementById('progressContainer');\r\n\t\tconst progressBar = document.getElementById('progressBar');\r\n\t\tconst configWarning = document.getElementById('configWarning');\r\n\t\tconst generateScriptBtn = document.getElementById('generateScriptBtn');\r\n\t\tconst generatedScripts = document.getElementById('generatedScripts');\r\n\t\tconst generateAudioBtn = document.getElementById('generateAudioBtn');\r\n\t\tconst mainPlayer = document.getElementById('mainPlayer');\r\n\t\tconst playAllBtn = document.getElementById('playAllBtn');\r\n\t\tconst pauseBtn = document.getElementById('pauseBtn');\r\n\t\tconst downloadMergedBtn = document.getElementById('downloadMergedBtn');\r\n\t\tconst scriptCards = document.getElementById('scriptCards');\r\n\t\t\r\n\t\t\/\/ Global Variables\r\n\t\tlet audioSegments = [];\r\n\t\tlet mergedAudioBlob = null;\r\n\t\tlet speakerCounter = 1;\r\n\t\tconst supportedTypes = ['txt', 'srt', 'json', 'xml', 'html', 'md', 'csv', 'ass', 'ssa', 'docx', 'pptx', 'pdf'];\r\n\t\tlet currentFileExtension = ''; \/\/ Store current file extension\r\n\t\tlet currentWordCount = 0; \/\/ Store current word count\r\n\t\t\r\n\t\t\/\/ =====================================\r\n\t\t\/\/ Audio Streaming Functions\r\n\t\t\/\/ =====================================\r\n\r\n\t\t\/\/ Japanese Voice Mapping\r\n\t\tconst JAPANESE_VOICE_MAPPING = {\r\n\t\t\t\"Friendly_Person\": \"Japanese_IntellectualSenior\",\r\n\t\t\t\"Lively_Girl\": \"Japanese_DecisivePrincess\",\r\n\t\t\t\"Patient_Man\": \"Japanese_LoyalKnight\",\r\n\t\t\t\"Determined_Man\": \"Japanese_DominantMan\",\r\n\t\t\t\"Deep_Voice_Man\": \"Japanese_SeriousCommander\",\r\n\t\t\t\"Imposing_Manner\": \"Japanese_ColdQueen\",\r\n\t\t\t\"Lovely_Girl\": \"Japanese_DependableWoman\",\r\n\t\t\t\"Elegant_Man\": \"Japanese_GentleButler\",\r\n\t\t\t\"Wise_Woman\": \"Japanese_KindLady\",\r\n\t\t\t\"Calm_Woman\": \"Japanese_CalmLady\",\r\n\t\t\t\"Decent_Boy\": \"Japanese_OptimisticYouth\",\r\n\t\t\t\"Casual_Guy\": \"Japanese_GenerousIzakayaOwner\",\r\n\t\t\t\"Abbess\": \"Japanese_FemaleBroadcaster\",\r\n\t\t\t\"Young_Knight\": \"Japanese_InnocentBoy\",\r\n\t\t\t\"Sweet_Girl_2\": \"Japanese_GracefulMaiden\",\r\n\t\t\t\"Exuberant_Girl\": \"Japanese_Bright_Girl\",\r\n\t\t\t\"Inspirational_girl\": \"Japanese_DependableWoman\"\r\n\t\t};\r\n\r\n\t\tfunction getAdjustedVoiceId(voiceId, language) {\r\n\t\t\tif (language && language.toLowerCase().includes('japanese')) {\r\n\t\t\t\tif (JAPANESE_VOICE_MAPPING[voiceId]) {\r\n\t\t\t\t\tconsole.log(`Mapping voice ${voiceId} to ${JAPANESE_VOICE_MAPPING[voiceId]} for Japanese language`);\r\n\t\t\t\t\treturn JAPANESE_VOICE_MAPPING[voiceId];\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\treturn voiceId;\r\n\t\t}\r\n\r\n\t\tasync function generateSingleAudioStream(text, voiceId, language) {\r\n\t\t\tconst finalVoiceId = getAdjustedVoiceId(voiceId, language);\r\n\t\t\t\r\n\t\t\tconst performMinimaxCall = async () => {\r\n\t\t\t\tconst url = MINIMAX_API_URL;\r\n\t\t\t\tconst requestBody = {\r\n\t\t\t\t\tmodel: \"speech-2.6-turbo\",\r\n\t\t\t\t\ttext: text,\r\n\t\t\t\t\tstream: true,\r\n\t\t\t\t\toutput_format: \"hex\",\r\n\t\t\t\t\tvoice_setting: { voice_id: finalVoiceId, speed: 1.0, vol: 1.0, pitch: 0 },\r\n\t\t\t\t\taudio_setting: { sample_rate: 32000, bitrate: 128000, format: \"mp3\", channel: 1 },\r\n\t\t\t\t\tlanguage_boost: language\r\n\t\t\t\t};\r\n\t\t\t\t\r\n\t\t\t\tconst response = await fetch(url, {\r\n\t\t\t\t\tmethod: 'POST',\r\n\t\t\t\t\theaders: {\r\n\t\t\t\t\t\t'Authorization': `Bearer ${MINIMAX_API_KEY}`,\r\n\t\t\t\t\t\t'Content-Type': 'application\/json',\r\n\t\t\t\t\t\t'X-Origin-PW': 'origin',\r\n\t\t\t\t\t\t'X-Minimax-Group': MINIMAX_GROUP_ID\r\n\t\t\t\t\t},\r\n\t\t\t\t\tbody: JSON.stringify(requestBody)\r\n\t\t\t\t});\r\n\t\t\t\t\r\n\t\t\t\tif (!response.ok) {\r\n\t\t\t\t\tconst errorText = await response.text();\r\n\t\t\t\t\tthrow new Error(`HTTP ${response.status}: ${errorText}`);\r\n\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\t\tconst reader = response.body.getReader();\r\n\t\t\t\tconst decoder = new TextDecoder();\r\n\t\t\t\tlet buffer = '';\r\n\t\t\t\tlet finalAudioHex = null;\r\n\t\t\t\tlet isComplete = false;\r\n\t\t\t\t\r\n\t\t\t\ttry {\r\n\t\t\t\t\twhile (!isComplete) {\r\n\t\t\t\t\t\tconst { done, value } = await reader.read();\r\n\t\t\t\t\t\tif (done) break;\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\tbuffer += decoder.decode(value, { stream: true });\r\n\t\t\t\t\t\tlet startPos = 0;\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\twhile (startPos < buffer.length) {\r\n\t\t\t\t\t\t\tconst openBrace = buffer.indexOf('{', startPos);\r\n\t\t\t\t\t\t\tif (openBrace === -1) break;\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\tlet braceCount = 0;\r\n\t\t\t\t\t\t\tlet endPos = openBrace;\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\tfor (let i = openBrace; i < buffer.length; i++) {\r\n\t\t\t\t\t\t\t\tif (buffer[i] === '{') braceCount++;\r\n\t\t\t\t\t\t\t\telse if (buffer[i] === '}') {\r\n\t\t\t\t\t\t\t\t\tbraceCount--;\r\n\t\t\t\t\t\t\t\t\tif (braceCount === 0) {\r\n\t\t\t\t\t\t\t\t\t\tendPos = i;\r\n\t\t\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\tif (braceCount === 0 && endPos > openBrace) {\r\n\t\t\t\t\t\t\t\tconst jsonStr = buffer.substring(openBrace, endPos + 1);\r\n\t\t\t\t\t\t\t\ttry {\r\n\t\t\t\t\t\t\t\t\tconst jsonData = JSON.parse(jsonStr);\r\n\t\t\t\t\t\t\t\t\tif (jsonData.data && jsonData.data.audio && jsonData.data.status === 2) {\r\n\t\t\t\t\t\t\t\t\t\tfinalAudioHex = jsonData.data.audio;\r\n\t\t\t\t\t\t\t\t\t\tisComplete = true;\r\n\t\t\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t} catch (parseError) {\r\n\t\t\t\t\t\t\t\t\tconsole.warn('Failed to parse JSON chunk:', parseError);\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\tstartPos = endPos + 1;\r\n\t\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\tif (startPos > 0) {\r\n\t\t\t\t\t\t\tbuffer = buffer.substring(startPos);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t} finally {\r\n\t\t\t\t\treader.releaseLock();\r\n\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\t\tif (!finalAudioHex) {\r\n\t\t\t\t\tthrow new Error('No final audio data received');\r\n\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\t\tconst audioData = hexStringToBytes(finalAudioHex);\r\n\t\t\t\t\r\n\t\t\t\treturn audioData;\r\n\t\t\t};\r\n\t\t\t\r\n\t\t\t\/\/ Use retry wrapper with key refresh\r\n\t\t\treturn await retryWithKeyRefresh(performMinimaxCall, 'minimax', 3);\r\n\t\t}\r\n\t\t\r\n\t\tfunction hexStringToBytes(hexString) {\r\n\t\t\tconst bytes = new Uint8Array(hexString.length \/ 2);\r\n\t\t\tfor (let i = 0; i < hexString.length; i += 2) {\r\n\t\t\t\tbytes[i \/ 2] = parseInt(hexString.substr(i, 2), 16);\r\n\t\t\t}\r\n\t\t\treturn bytes;\r\n\t\t}\r\n\t\t\r\n\t\tfunction concatenateUint8Arrays(a, b) {\r\n\t\t\tconst result = new Uint8Array(a.length + b.length);\r\n\t\t\tresult.set(a);\r\n\t\t\tresult.set(b, a.length);\r\n\t\t\treturn result;\r\n\t\t}\r\n\t\t\r\n\t\tfunction createAudioBlob(audioData) {\r\n\t\t\treturn new Blob([audioData], { type: 'audio\/mpeg' });\r\n\t\t}\r\n\t\t\r\n\t\tfunction concatenateAllAudioSegments() {\r\n\t\t\tif (audioSegments.length === 0) return null;\r\n\t\t\t\r\n\t\t\tlet totalAudioData = new Uint8Array(0);\r\n\t\t\taudioSegments.forEach(segment => {\r\n\t\t\t\ttotalAudioData = concatenateUint8Arrays(totalAudioData, segment.audioData);\r\n\t\t\t});\r\n\t\t\t\r\n\t\t\tmergedAudioBlob = createAudioBlob(totalAudioData);\r\n\t\t\tconst mergedUrl = URL.createObjectURL(mergedAudioBlob);\r\n\t\t\tmainPlayer.src = mergedUrl;\r\n\t\t\tdownloadMergedBtn.style.display = 'inline-block';\r\n\t\t\t\r\n\t\t\treturn mergedAudioBlob;\r\n\t\t}\r\n\t\t\r\n\t\tfunction downloadMergedAudio() {\r\n\t\t\tif (mergedAudioBlob) {\r\n\t\t\t\tconst url = URL.createObjectURL(mergedAudioBlob);\r\n\t\t\t\tconst a = document.createElement('a');\r\n\t\t\t\ta.href = url;\r\n\t\t\t\ta.download = 'merged_conversation.mp3';\r\n\t\t\t\tdocument.body.appendChild(a);\r\n\t\t\t\ta.click();\r\n\t\t\t\tdocument.body.removeChild(a);\r\n\t\t\t\tURL.revokeObjectURL(url);\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\t\t\/\/ =====================================\r\n\t\t\/\/ PDF.js Setup\r\n\t\t\/\/ =====================================\r\n\t\tpdfjsLib.GlobalWorkerOptions.workerSrc = 'https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pdf.js\/3.11.174\/pdf.worker.min.js';\r\n\t\t\r\n\t\t\/\/ =====================================\r\n\t\t\/\/ File Handling Functions\r\n\t\t\/\/ =====================================\r\n\t\tfunction getFileExtension(filename) {\r\n\t\t\treturn filename.split('.').pop().toLowerCase();\r\n\t\t}\r\n\t\t\r\n\t\tfunction formatFileSize(bytes) {\r\n\t\t\tif (bytes === 0) return '0 Bytes';\r\n\t\t\tconst k = 1024;\r\n\t\t\tconst sizes = ['Bytes', 'KB', 'MB', 'GB'];\r\n\t\t\tconst i = Math.floor(Math.log(bytes) \/ Math.log(k));\r\n\t\t\treturn parseFloat((bytes \/ Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\r\n\t\t}\r\n\t\t\r\n\t\tfunction readTextFile(file) {\r\n\t\t\treturn new Promise((resolve, reject) => {\r\n\t\t\t\tconst reader = new FileReader();\r\n\t\t\t\treader.onload = (e) => resolve(e.target.result);\r\n\t\t\t\treader.onerror = (e) => reject(new Error('Failed to read file'));\r\n\t\t\t\treader.readAsText(file);\r\n\t\t\t});\r\n\t\t}\r\n\t\t\r\n\t\tasync function extractDocxText(file) {\r\n\t\t\ttry {\r\n\t\t\t\tconst arrayBuffer = await file.arrayBuffer();\r\n\t\t\t\tconst result = await mammoth.extractRawText({ arrayBuffer });\r\n\t\t\t\treturn result.value;\r\n\t\t\t} catch (error) {\r\n\t\t\t\tthrow new Error('Failed to read DOCX file: ' + error.message);\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\t\tasync function extractPptxText(file) {\r\n\t\t\ttry {\r\n\t\t\t\tconst arrayBuffer = await file.arrayBuffer();\r\n\t\t\t\tconst zip = await JSZip.loadAsync(arrayBuffer);\r\n\t\t\t\tlet text = '';\r\n\t\t\t\t\r\n\t\t\t\tconst slideFiles = Object.keys(zip.files).filter(name => \r\n\t\t\t\t\tname.startsWith('ppt\/slides\/slide') && name.endsWith('.xml')\r\n\t\t\t\t);\r\n\t\t\t\t\r\n\t\t\t\tfor (const slideFile of slideFiles) {\r\n\t\t\t\t\tconst content = await zip.file(slideFile).async('string');\r\n\t\t\t\t\t\/\/ Simple XML parsing to extract text\r\n\t\t\t\t\tconst textMatches = content.match(\/<a:t[^>]*>([^<]*)<\\\/a:t>\/g);\r\n\t\t\t\t\tif (textMatches) {\r\n\t\t\t\t\t\ttextMatches.forEach(match => {\r\n\t\t\t\t\t\t\tconst textContent = match.replace(\/<[^>]*>\/g, '');\r\n\t\t\t\t\t\t\tif (textContent.trim()) {\r\n\t\t\t\t\t\t\t\ttext += textContent + '\\n';\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t});\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\t\treturn text;\r\n\t\t\t} catch (error) {\r\n\t\t\t\tthrow new Error('Failed to read PPTX file: ' + error.message);\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\t\t\/**\r\n\t\t * Extract text from PDF files\r\n\t\t * @param {File} file - The PDF file\r\n\t\t * @returns {Promise<string>} The extracted text\r\n\t\t *\/\r\n\t\t\/**\r\n\t\t * Update visibility of file info rows based on their values\r\n\t\t *\/\r\n\t\tfunction updateFileInfoVisibility() {\r\n\t\t\tconst fileDetailRows = document.querySelectorAll('.file-detail-row');\r\n\t\t\tfileDetailRows.forEach(row => {\r\n\t\t\t\tconst valueElement = row.querySelector('.file-detail-value');\r\n\t\t\t\tif (valueElement) {\r\n\t\t\t\t\tconst value = valueElement.textContent.trim();\r\n\t\t\t\t\tif (value === '-' || value === 'N\/A' || value === '') {\r\n\t\t\t\t\t\trow.classList.add('hidden');\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\trow.classList.remove('hidden');\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t});\r\n\t\t}\r\n\r\n\t\t\/\/ After processing the file content, update the stats\r\n\t\tfunction updateDocumentStats(content) {\r\n\t\t\t\/\/ Calculate stats\r\n\t\t\tconst words = content.trim().split(\/\\s+\/).filter(word => word.length > 0);\r\n\t\t\tconst wordCount = words.length;\r\n\t\t\tconst charCount = content.length;\r\n\t\t\t\r\n\t\t\t\/\/ Update UI\r\n\t\t\tdocument.getElementById('wordCount').textContent = wordCount.toLocaleString();\r\n\t\t\tdocument.getElementById('charCount').textContent = charCount.toLocaleString();\r\n\t\t\t\r\n\t\t\t\/\/ Update visibility after setting values\r\n\t\t\tupdateFileInfoVisibility();\r\n\t\t}\r\n\r\n\t\t\/\/ For PDF files, update the page count in the extractPdfText function:\r\n\t\tasync function extractPdfText(file) {\r\n\t\t\ttry {\r\n\t\t\t\tconst arrayBuffer = await file.arrayBuffer();\r\n\t\t\t\tconst pdf = await pdfjsLib.getDocument(arrayBuffer).promise;\r\n\t\t\t\t\r\n\t\t\t\t\/\/ Update page count\r\n\t\t\t\tdocument.getElementById('pageCount').textContent = pdf.numPages;\r\n\t\t\t\t\r\n\t\t\t\tlet fullText = '';\r\n\t\t\t\tfor (let i = 1; i <= pdf.numPages; i++) {\r\n\t\t\t\t\tconst page = await pdf.getPage(i);\r\n\t\t\t\t\tconst textContent = await page.getTextContent();\r\n\t\t\t\t\tfullText += textContent.items.map(item => item.str).join(' ') + '\\n\\n';\r\n\t\t\t\t\tupdateProgress(30 + (i \/ pdf.numPages * 50));\r\n\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\t\t\/\/ Update visibility after setting page count\r\n\t\t\t\tupdateFileInfoVisibility();\r\n\t\t\t\t\r\n\t\t\t\treturn fullText;\r\n\t\t\t} catch (error) {\r\n\t\t\t\tthrow new Error('Failed to read PDF file: ' + error.message);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t\r\n\t\t\/**\r\n\t\t * Process file based on its extension\r\n\t\t * @param {File} file - The file to process\r\n\t\t *\/\r\n\t\tasync function extractTextFromFile(file) {\r\n\t\t\ttry {\r\n\t\t\t\tshowProgress(true);\r\n\t\t\t\tupdateProgress(30);\r\n\t\t\t\t\r\n\t\t\t\tconst extension = getFileExtension(file.name);\r\n\t\t\t\tlet content = '';\r\n\t\t\t\t\r\n\t\t\t\t\/\/ Handle different file types\r\n\t\t\t\tif (extension === 'pdf') {\r\n\t\t\t\t\tcontent = await extractPdfText(file);\r\n\t\t\t\t} else if (extension === 'docx') {\r\n\t\t\t\t\tcontent = await extractDocxText(file);\r\n\t\t\t\t} else if (extension === 'pptx') {\r\n\t\t\t\t\tcontent = await extractPptxText(file);\r\n\t\t\t\t} else {\r\n\t\t\t\t\t\/\/ For text-based files\r\n\t\t\t\t\tcontent = await readTextFile(file);\r\n\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\t\tupdateProgress(80);\r\n\t\t\t\t\r\n\t\t\t\t\/\/ Apply word limit (800 words)\r\n\t\t\t\tconst words = content.trim().split(\/\\s+\/).filter(word => word.length > 0);\r\n\t\t\t\tlet limitedContent = '';\r\n\t\t\t\tlet wordCount = 0;\r\n\t\t\t\t\r\n\t\t\t\tfor (const word of words) {\r\n\t\t\t\t\tif (wordCount < 800) {\r\n\t\t\t\t\t\tlimitedContent += word + ' ';\r\n\t\t\t\t\t\twordCount++;\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\t\treturn limitedContent.trim();\r\n\t\t\t} catch (error) {\r\n\t\t\t\tthrow error;\r\n\t\t\t} finally {\r\n\t\t\t\tupdateProgress(100);\r\n\t\t\t\tsetTimeout(() => showProgress(false), 1000);\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\t\t\/\/ Show\/hide progress bar\r\n\t\tfunction showProgress(show) {\r\n\t\t\tprogressContainer.style.display = show ? 'block' : 'none';\r\n\t\t}\r\n\t\t\r\n\t\t\/\/ Update progress bar\r\n\t\tfunction updateProgress(percent) {\r\n\t\t\tprogressBar.style.width = percent + '%';\r\n\t\t}\r\n\t\t\r\n\t\t\/\/ ===================\r\n\t\t\/\/ File Event Handlers\r\n\t\t\/\/ ===================\r\n\t\tdropArea.addEventListener('click', () => fileInput.click());\r\n\t\tfileInput.addEventListener('change', handleFileSelect);\r\n\t\t\r\n\t\t['dragover', 'dragenter'].forEach(event => {\r\n\t\t\tdropArea.addEventListener(event, (e) => {\r\n\t\t\t\te.preventDefault();\r\n\t\t\t\tdropArea.classList.add('active');\r\n\t\t\t});\r\n\t\t});\r\n\t\t\r\n\t\t['dragleave', 'dragend'].forEach(event => {\r\n\t\t\tdropArea.addEventListener(event, () => {\r\n\t\t\t\tdropArea.classList.remove('active');\r\n\t\t\t});\r\n\t\t});\r\n\t\t\r\n\t\tdropArea.addEventListener('drop', async (e) => {\r\n\t\t\te.preventDefault();\r\n\t\t\tdropArea.classList.remove('active');\r\n\t\t\t\r\n\t\t\tif (e.dataTransfer.files.length > 0) {\r\n\t\t\t\tconst file = e.dataTransfer.files[0];\r\n\t\t\t\tawait handleFile(file);\r\n\t\t\t}\r\n\t\t});\r\n\t\t\r\n\t\tfunction handleFileSelect(e) {\r\n\t\t\tconst file = e.target.files[0];\r\n\t\t\tif (file) handleFile(file);\r\n\t\t}\r\n\t\t\r\n\t\tasync function handleFile(file) {\r\n\t\t\tconst extension = getFileExtension(file.name);\r\n\t\t\tcurrentFileExtension = extension; \/\/ Store extension for validation\r\n\t\t\t\r\n\t\t\tif (!supportedTypes.includes(extension)) {\r\n\t\t\t\talert(`Unsupported file type: ${extension}`);\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\t\/\/ Display file information\r\n\t\t\tfileNameDisplay.textContent = file.name;\r\n\t\t\tfileTypeDisplay.textContent = extension.toUpperCase();\r\n\t\t\tfileSizeDisplay.textContent = formatFileSize(file.size);\r\n\t\t\t\r\n\t\t\t\/\/ Reset page count for non-PDF files\r\n\t\t\tif (extension !== 'pdf') {\r\n\t\t\t\tdocument.getElementById('pageCount').textContent = 'N\/A';\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\t\/\/ Update visibility before showing\r\n\t\t\tupdateFileInfoVisibility();\r\n\t\t\t\r\n\t\t\t\/\/ Show file info\r\n\t\t\tfileInfo.style.display = 'block';\r\n\t\t\t\r\n\t\t\ttry {\r\n\t\t\t\tconst content = await extractTextFromFile(file);\r\n\t\t\t\tuserInput.value = content;\r\n\t\t\t\tupdateDocumentStats(content);\r\n\t\t\t\t\r\n\t\t\t\t\/\/ Store word count for validation\r\n\t\t\t\tconst words = content.trim().split(\/\\s+\/).filter(word => word.length > 0);\r\n\t\t\t\tcurrentWordCount = words.length;\r\n\t\t\t} catch (error) {\r\n\t\t\t\talert(`Error processing file: ${error.message}`);\r\n\t\t\t\tconsole.error(error);\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\t\t\/\/ ===================\r\n\t\t\/\/ Speaker Configuration\r\n\t\t\/\/ ===================\r\n\t\tfunction populateDropdowns(card) {\r\n\t\t\tconst voiceSelect = card.querySelector('.voice-id');\r\n\t\t\tconst languageSelect = card.querySelector('.language');\r\n\t\t\t\r\n\t\t\t\/\/ Populate voice options\r\n\t\t\tvoiceSelect.innerHTML = '';\r\n\t\t\tVOICE_IDS.forEach(voice => {\r\n\t\t\t\tconst option = document.createElement('option');\r\n\t\t\t\toption.value = voice;\r\n\t\t\t\toption.textContent = voice.replace(\/_\/g, ' ');\r\n\t\t\t\tvoiceSelect.appendChild(option);\r\n\t\t\t});\r\n\t\t\t\r\n\t\t\t\/\/ Populate language options\r\n\t\t\tlanguageSelect.innerHTML = '';\r\n\t\t\tLANGUAGES.forEach(lang => {\r\n\t\t\t\tconst option = document.createElement('option');\r\n\t\t\t\toption.value = lang.code;\r\n\t\t\t\toption.textContent = lang.name;\r\n\t\t\t\tlanguageSelect.appendChild(option);\r\n\t\t\t});\r\n\t\t}\r\n\t\t\r\n\t\tfunction addSpeaker() {\r\n\t\t\tconst speakerCards = document.getElementById('speakerCards');\r\n\t\t\tconst currentCount = speakerCards.children.length;\r\n\t\t\t\r\n\t\t\tif (currentCount >= 3) {\r\n\t\t\t\talert('Maximum 3 speakers allowed');\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tspeakerCounter++;\r\n\t\t\tconst defaultName = speakerCounter === 2 ? 'Alice' : `Speaker ${speakerCounter}`;\r\n\t\t\t\r\n\t\t\tconst newCard = document.createElement('div');\r\n\t\t\tnewCard.className = 'speaker-card';\r\n\t\t\tnewCard.setAttribute('data-speaker', speakerCounter);\r\n\t\t\tnewCard.innerHTML = `\r\n\t\t\t\t<h4>Speaker ${speakerCounter}<\/h4>\r\n\t\t\t\t<div class=\"control-group\">\r\n\t\t\t\t\t<label>Name:<\/label>\r\n\t\t\t\t\t<input type=\"text\" class=\"speaker-name\" value=\"${defaultName}\" placeholder=\"Speaker Name\">\r\n\t\t\t\t<\/div>\r\n\t\t\t\t<div class=\"control-group\">\r\n\t\t\t\t\t<label>Voice:<\/label>\r\n\t\t\t\t\t<select class=\"voice-id\"><\/select>\r\n\t\t\t\t<\/div>\r\n\t\t\t\t<div class=\"control-group\">\r\n\t\t\t\t\t<label>Language:<\/label>\r\n\t\t\t\t\t<select class=\"language\"><\/select>\r\n\t\t\t\t<\/div>\r\n\t\t\t\t<button class=\"remove-speaker-btn\" onclick=\"removeSpeaker(this)\">Remove Speaker<\/button>\r\n\t\t\t`;\r\n\t\t\t\r\n\t\t\tspeakerCards.appendChild(newCard);\r\n\t\t\tpopulateDropdowns(newCard);\r\n\t\t\tupdateSpeakerCount();\r\n\t\t\tvalidateConfiguration();\r\n\t\t}\r\n\t\t\r\n\t\tfunction removeSpeaker(button) {\r\n\t\t\tconst card = button.closest('.speaker-card');\r\n\t\t\tconst speakerCards = document.getElementById('speakerCards');\r\n\t\t\t\r\n\t\t\tif (speakerCards.children.length <= 1) {\r\n\t\t\t\talert('At least one speaker is required');\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tcard.remove();\r\n\t\t\tupdateSpeakerCount();\r\n\t\t\tvalidateConfiguration();\r\n\t\t}\r\n\t\t\r\n\t\tfunction updateSpeakerCount() {\r\n\t\t\tconst count = document.getElementById('speakerCards').children.length;\r\n\t\t\tdocument.getElementById('speakerCount').textContent = `${count} Speaker${count > 1 ? 's' : ''}`;\r\n\t\t}\r\n\t\t\r\n\t\tfunction validateConfiguration() {\r\n\t\t\tconst speakerCards = document.querySelectorAll('.speaker-card');\r\n\t\t\tlet isValid = true;\r\n\t\t\t\r\n\t\t\tspeakerCards.forEach(card => {\r\n\t\t\t\tconst nameInput = card.querySelector('.speaker-name');\r\n\t\t\t\tif (!nameInput.value.trim()) {\r\n\t\t\t\t\tisValid = false;\r\n\t\t\t\t}\r\n\t\t\t});\r\n\t\t\t\r\n\t\t\tif (isValid) {\r\n\t\t\t\tconfigWarning.style.display = 'none';\r\n\t\t\t\tgenerateScriptBtn.disabled = false;\r\n\t\t\t} else {\r\n\t\t\t\tconfigWarning.style.display = 'block';\r\n\t\t\t\tgenerateScriptBtn.disabled = true;\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\t\tfunction getSpeakerConfiguration() {\r\n\t\t\tconst speakerCards = document.querySelectorAll('.speaker-card');\r\n\t\t\tconst names = [];\r\n\t\t\tconst languages = [];\r\n\t\t\tconst configs = {};\r\n\t\t\t\r\n\t\t\tspeakerCards.forEach(card => {\r\n\t\t\t\tconst name = card.querySelector('.speaker-name').value.trim();\r\n\t\t\t\tconst voiceId = card.querySelector('.voice-id').value;\r\n\t\t\t\tconst language = card.querySelector('.language').value;\r\n\t\t\t\t\r\n\t\t\t\tif (name) {\r\n\t\t\t\t\tnames.push(name);\r\n\t\t\t\t\tlanguages.push(language);\r\n\t\t\t\t\tconfigs[name] = { voiceId, language };\r\n\t\t\t\t}\r\n\t\t\t});\r\n\t\t\t\r\n\t\t\treturn { names, languages, configs };\r\n\t\t}\r\n\t\t\r\n\t\t\/\/ ===================\r\n\t\t\/\/ Coze API Functions\r\n\t\t\/\/ ===================\r\n\t\tfunction extractScriptsFromOutput(output) {\r\n\t\t\ttry {\r\n\t\t\t\t\/\/ Handle object input directly\r\n\t\t\t\tif (typeof output === 'object' && output !== null) {\r\n\t\t\t\t\t\/\/ New format: script array in root.conversation_script\r\n\t\t\t\t\tif (output.conversation_script && Array.isArray(output.conversation_script)) {\r\n\t\t\t\t\t\treturn output.conversation_script;\r\n\t\t\t\t\t}\r\n\t\t\t\t\t\/\/ Alternative format: script array in root.script\r\n\t\t\t\t\tif (output.script && Array.isArray(output.script)) {\r\n\t\t\t\t\t\treturn output.script;\r\n\t\t\t\t\t}\r\n\t\t\t\t\t\/\/ Old format: script array in root.output\r\n\t\t\t\t\tif (output.output) {\r\n\t\t\t\t\t\treturn extractScriptsFromOutput(output.output);\r\n\t\t\t\t\t}\r\n\t\t\t\t\t\/\/ Check any key for array\r\n\t\t\t\t\tfor (const key in output) {\r\n\t\t\t\t\t\tif (Array.isArray(output[key])) {\r\n\t\t\t\t\t\t\treturn output[key];\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t\treturn null;\r\n\t\t\t\t}\r\n\r\n\t\t\t\t\/\/ Handle string input\r\n\t\t\t\tif (typeof output === 'string') {\r\n\t\t\t\t\t\/\/ Attempt to parse as JSON\r\n\t\t\t\t\ttry {\r\n\t\t\t\t\t\tconst parsed = JSON.parse(output);\r\n\t\t\t\t\t\treturn extractScriptsFromOutput(parsed);\r\n\t\t\t\t\t} catch (e) {\r\n\t\t\t\t\t\t\/\/ Try extracting JSON from markdown code blocks\r\n\t\t\t\t\t\tconst match = output.match(\/```json\\n([\\s\\S]*?)\\n```\/);\r\n\t\t\t\t\t\tif (match) {\r\n\t\t\t\t\t\t\ttry {\r\n\t\t\t\t\t\t\t\treturn JSON.parse(match[1]);\r\n\t\t\t\t\t\t\t} catch (e) {\r\n\t\t\t\t\t\t\t\tconsole.error('JSON parse error in markdown block:', e);\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\t\treturn null;\r\n\t\t\t} catch (error) {\r\n\t\t\t\tconsole.error('Error extracting scripts:', error);\r\n\t\t\t\treturn null;\r\n\t\t\t}\r\n\t\t}            \r\n\t\t\r\n\t\tfunction cleanScript(script) {\r\n\t\t\treturn script.replace(\/<#[\\d.]+#>\/g, '').trim();\r\n\t\t}\r\n\t\t\r\n\t\tfunction buildCozeQuery() {\r\n\t\t\tconst inputText = userInput.value.trim();\r\n\t\t\tconst { names, languages } = getSpeakerConfiguration();\r\n\r\n\t\t\tconst queryData = {\r\n\t\t\t\tnumber_of_speakers: names.length,\r\n\t\t\t\tspeaker_names: names,\r\n\t\t\t\tspeakers_languages: languages,\r\n\t\t\t\tconversation_content: inputText\r\n\t\t\t};\r\n\r\n\t\t\tconst finalQuery = {\r\n\t\t\t\tNewUserQuery: \"Please create a conversation script based on the following requirement:\",\r\n\t\t\t\t...queryData\r\n\t\t\t};\r\n\r\n\t\t\treturn JSON.stringify(finalQuery);\r\n\t\t}\r\n\t\t\r\n\t\t\/\/ Cleaning function to remove ```json tags\r\n\t\tfunction cleanMarkdownCodeBlocks(text) {\r\n\t\t\treturn text.replace(\/```json\/g, '').replace(\/```\/g, '');\r\n\t\t}\r\n\t\t\r\n\t\tasync function sendCozeRequest() {\r\n\t\t\tconst inputText = userInput.value.trim();\r\n\t\t\tif (!inputText) {\r\n\t\t\t\talert('Please enter some text first');\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\t\/\/ Validate word count before proceeding\r\n\t\t\t\/\/ If file was uploaded, check stored word count; otherwise check input text directly\r\n\t\t\tlet wordCountToCheck = currentWordCount;\r\n\t\t\tif (wordCountToCheck === 0 && inputText) {\r\n\t\t\t\t\/\/ Calculate word count from input text\r\n\t\t\t\tconst words = inputText.trim().split(\/\\s+\/).filter(word => word.length > 0);\r\n\t\t\t\twordCountToCheck = words.length;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tif (wordCountToCheck === 0) {\r\n\t\t\t\tif (currentFileExtension === 'pdf') {\r\n\t\t\t\t\talert('No extractable text found in your PDF file. The PDF may be image-based or scanned. Please consider using an OCR tool to extract text from the file first, then try again.');\r\n\t\t\t\t} else {\r\n\t\t\t\t\talert('No extractable text found. Please enter or upload text with readable content and try again.');\r\n\t\t\t\t}\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tvalidateConfiguration();\r\n\t\t\tif (generateScriptBtn.disabled) {\r\n\t\t\t\talert('Please configure all speakers properly');\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tgenerateScriptBtn.disabled = true;\r\n\t\t\tgeneratedScripts.value = 'Generating scripts...';\r\n\t\t\t\r\n\t\t\tconst performCozeCall = async () => {\r\n\t\t\t\tconst query = buildCozeQuery();\r\n\t\t\t\t\r\n\t\t\t\tconst payload = {\r\n\t\t\t\t\tbot_id: COZE_BOT_ID,\r\n\t\t\t\t\tuser: USER_ID,\r\n\t\t\t\t\tquery: query,\r\n\t\t\t\t\tstream: false\r\n\t\t\t\t};\r\n\t\t\t\t\r\n\t\t\t\tconst response = await fetch(COZE_API_URL, {\r\n\t\t\t\t\tmethod: 'POST',\r\n\t\t\t\t\theaders: {\r\n\t\t\t\t\t\t'Content-Type': 'application\/json',\r\n\t\t\t\t\t\t'Authorization': `Bearer ${COZE_API_KEY}`,\r\n\t\t\t\t\t\t'Accept': '*\/*'\r\n\t\t\t\t\t},\r\n\t\t\t\t\tbody: JSON.stringify(payload)\r\n\t\t\t\t});\r\n\t\t\t\t\r\n\t\t\t\tif (!response.ok) {\r\n\t\t\t\t\tconst errorText = await response.text();\r\n\t\t\t\t\tthrow new Error(`HTTP ${response.status}: ${errorText}`);\r\n\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\t\tconst json = await response.json();\r\n\t\t\t\treturn json;\r\n\t\t\t};\r\n\t\t\t\r\n\t\t\ttry {\r\n\t\t\t\t\/\/ Use retry wrapper with key refresh\r\n\t\t\t\tconst data = await retryWithKeyRefresh(performCozeCall, 'coze', 3);\r\n\t\t\t\t\r\n\t\t\t\tconst toolMsg = data.messages.find(msg => msg.type === 'tool_response');\r\n\r\n\t\t\t\tif (toolMsg) {\r\n\t\t\t\t\tconst content = JSON.parse(toolMsg.content);\r\n\t\t\t\t\tconst parsed = extractScriptsFromOutput(content.output);\r\n\t\t\t\t\t\r\n\t\t\t\t\tif (parsed && Array.isArray(parsed)) {\r\n\t\t\t\t\t\tconst cleanedScripts = parsed.map(script => ({\r\n\t\t\t\t\t\t\t...script,\r\n\t\t\t\t\t\t\tScript: script.Script ? cleanScript(script.Script) : script.Script\r\n\t\t\t\t\t\t}));\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\t\/\/ Clean markdown code blocks before displaying\r\n\t\t\t\t\t\tlet displayContent = JSON.stringify(cleanedScripts, null, 2);\r\n\t\t\t\t\t\tdisplayContent = cleanMarkdownCodeBlocks(displayContent);\r\n\t\t\t\t\t\tgeneratedScripts.value = displayContent;\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tlet outputContent = typeof content.output === 'string' \r\n\t\t\t\t\t\t\t? content.output \r\n\t\t\t\t\t\t\t: JSON.stringify(content.output, null, 2);\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\t\/\/ Clean markdown code blocks\r\n\t\t\t\t\t\toutputContent = cleanMarkdownCodeBlocks(outputContent);\r\n\t\t\t\t\t\tgeneratedScripts.value = outputContent;\r\n\t\t\t\t\t}\r\n\t\t\t\t} else {\r\n\t\t\t\t\tconst answerMsg = data.messages.find(msg => msg.type === 'answer');\r\n\t\t\t\t\tif (answerMsg) {\r\n\t\t\t\t\t\tconst content = JSON.parse(answerMsg.content);\r\n\t\t\t\t\t\tconst parsed = extractScriptsFromOutput(content.output);\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\tif (parsed && Array.isArray(parsed)) {\r\n\t\t\t\t\t\t\tconst cleanedScripts = parsed.map(script => ({\r\n\t\t\t\t\t\t\t\t...script,\r\n\t\t\t\t\t\t\t\tScript: script.Script ? cleanScript(script.Script) : script.Script\r\n\t\t\t\t\t\t\t}));\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\/\/ Clean markdown code blocks before displaying\r\n\t\t\t\t\t\t\tlet displayContent = JSON.stringify(cleanedScripts, null, 2);\r\n\t\t\t\t\t\t\tdisplayContent = cleanMarkdownCodeBlocks(displayContent);\r\n\t\t\t\t\t\t\tgeneratedScripts.value = displayContent;\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tlet outputContent = content.output || \"No parsable content found\";\r\n\t\t\t\t\t\t\tif (typeof outputContent !== 'string') {\r\n\t\t\t\t\t\t\t\toutputContent = JSON.stringify(outputContent, null, 2);\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\/\/ Clean markdown code blocks\r\n\t\t\t\t\t\t\toutputContent = cleanMarkdownCodeBlocks(outputContent);\r\n\t\t\t\t\t\t\tgeneratedScripts.value = outputContent;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tgeneratedScripts.value = \"No tool_response or answer message found\";\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\t} catch (error) {\r\n\t\t\t\tgeneratedScripts.value = `Error: ${error.message}`;\r\n\t\t\t} finally {\r\n\t\t\t\tgenerateScriptBtn.disabled = false;\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\t\t\/\/ ===================\r\n\t\t\/\/ Minimax API Functions\r\n\t\t\/\/ ===================\r\n\t\tasync function generateAllAudio() {\r\n\t\t\t\/\/ Parse the edited scripts from the textarea\r\n\t\t\tlet scriptsToUse;\r\n\t\t\ttry {\r\n\t\t\t\tscriptsToUse = JSON.parse(generatedScripts.value);\r\n\t\t\t} catch (error) {\r\n\t\t\t\talert('Failed to parse scripts. Please ensure the format is valid JSON');\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tif (!Array.isArray(scriptsToUse) || scriptsToUse.length === 0) {\r\n\t\t\t\talert('No valid scripts found. Please generate scripts first');\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tgenerateAudioBtn.disabled = true;\r\n\t\t\taudioSegments = [];\r\n\t\t\tscriptCards.innerHTML = '';\r\n\t\t\tdownloadMergedBtn.style.display = 'none';\r\n\t\t\t\r\n\t\t\t\/\/ Clear main player\r\n\t\t\tmainPlayer.src = '';\r\n\t\t\t\r\n\t\t\tconst { configs } = getSpeakerConfiguration();\r\n\t\t\t\r\n\t\t\ttry {\r\n\t\t\t\tfor (let i = 0; i < scriptsToUse.length; i++) {\r\n\t\t\t\t\tconst segment = scriptsToUse[i];\r\n\t\t\t\t\tif (!segment.Name || !segment.Script) continue;\r\n\t\t\t\t\t\r\n\t\t\t\t\tconst config = configs[segment.Name];\r\n\t\t\t\t\tif (!config) continue;\r\n\t\t\t\t\t\r\n\t\t\t\t\tconst langObj = LANGUAGES.find(l => l.code === config.language);\r\n\t\t\t\t\tconst minimaxLang = langObj ? langObj.minimax : config.language;\r\n\t\t\t\t\t\r\n\t\t\t\t\t\/\/ Clean the script before sending to Minimax\r\n\t\t\t\t\tconst cleanText = cleanScript(segment.Script);\r\n\t\t\t\t\t\r\n\t\t\t\t\t\/\/ Generate audio using streaming API\r\n\t\t\t\t\tconst audioData = await generateSingleAudioStream(cleanText, config.voiceId, minimaxLang);\r\n\t\t\t\t\t\r\n\t\t\t\t\t\/\/ Create individual blob for this segment\r\n\t\t\t\t\tconst segmentBlob = createAudioBlob(audioData);\r\n\t\t\t\t\tconst segmentUrl = URL.createObjectURL(segmentBlob);\r\n\t\t\t\t\t\r\n\t\t\t\t\taudioSegments.push({\r\n\t\t\t\t\t\tname: segment.Name,\r\n\t\t\t\t\t\tscript: segment.Script,\r\n\t\t\t\t\t\taudioData: audioData,\r\n\t\t\t\t\t\taudioUrl: segmentUrl,\r\n\t\t\t\t\t\tvoiceId: config.voiceId,\r\n\t\t\t\t\t\tlanguage: config.language\r\n\t\t\t\t\t});\r\n\t\t\t\t\t\r\n\t\t\t\t\t\/\/ Create script card with individual audio player\r\n\t\t\t\t\tconst card = document.createElement('div');\r\n\t\t\t\t\tcard.className = 'script-card';\r\n\t\t\t\t\tcard.innerHTML = `\r\n\t\t\t\t\t\t<h4>${segment.Name}<\/h4>\r\n\t\t\t\t\t\t<p>${segment.Script}<\/p>\r\n\t\t\t\t\t\t<audio controls src=\"${segmentUrl}\" style=\"width: 100%; margin-top: 10px;\"><\/audio>\r\n\t\t\t\t\t`;\r\n\t\t\t\t\tscriptCards.appendChild(card);\r\n\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\t\t\/\/ Automatically merge all audio segments\r\n\t\t\t\tif (audioSegments.length > 0) {\r\n\t\t\t\t\tconcatenateAllAudioSegments();\r\n\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\t} catch (error) {\r\n\t\t\t\talert(`Error generating audio: ${error.message}`);\r\n\t\t\t} finally {\r\n\t\t\t\tgenerateAudioBtn.disabled = false;\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\t\t\/\/ ===================\r\n\t\t\/\/ Audio Player Functions\r\n\t\t\/\/ ===================\r\n\t\tplayAllBtn.addEventListener('click', () => {\r\n\t\t\tif (mainPlayer.src) {\r\n\t\t\t\tmainPlayer.play();\r\n\t\t\t} else {\r\n\t\t\t\talert('No merged audio available. Please generate audio first.');\r\n\t\t\t}\r\n\t\t});\r\n\t\t\r\n\t\tpauseBtn.addEventListener('click', () => {\r\n\t\t\tmainPlayer.pause();\r\n\t\t});\r\n\r\n\t\t\/\/ Event listeners for download functionality\r\n\t\tdownloadMergedBtn.addEventListener('click', downloadMergedAudio);\r\n\t\t\r\n\t\t\/\/ ===================\r\n\t\t\/\/ Event Listeners\r\n\t\t\/\/ ===================\r\n\t\tgenerateScriptBtn.addEventListener('click', sendCozeRequest);\r\n\t\tgenerateAudioBtn.addEventListener('click', generateAllAudio);\r\n\t\t\r\n\t\t\/\/ Add event listeners for validation\r\n\t\tdocument.addEventListener('input', (e) => {\r\n\t\t\tif (e.target.classList.contains('speaker-name')) {\r\n\t\t\t\tvalidateConfiguration();\r\n\t\t\t}\r\n\t\t});\r\n\t\t\r\n\t\t\/\/ ===================\r\n\t\t\/\/ Initialize\r\n\t\t\/\/ ===================\r\n\t\twindow.addEventListener('DOMContentLoaded', async () => {\r\n\t\t\tpopulateDropdowns(document.querySelector('.speaker-card'));\r\n\t\t\tvalidateConfiguration();\r\n\t\t\t\r\n\t\t\t\/\/ Fetch API keys from backend\r\n\t\t\ttry {\r\n\t\t\t\tconsole.log('Initializing API keys from backend...');\r\n\t\t\t\tconst { cozeKey, minimaxKey } = await initializeApiKeys();\r\n\t\t\t\tCOZE_API_KEY_RUNTIME = cozeKey;\r\n\t\t\t\tMINIMAX_API_KEY_RUNTIME = minimaxKey;\r\n\t\t\t\tconsole.log('\u2713 All API keys initialized successfully');\r\n\t\t\t} catch (error) {\r\n\t\t\t\tconsole.error('Failed to initialize API keys:', error);\r\n\t\t\t\talert('Authorisation was not granted by the server. This may be due to a network issue. Please check your connection and try again.');\r\n\t\t\t}\r\n\t\t});\r\n\r\n\t\t\/\/ Clear API keys when page is closed\r\n\t\twindow.addEventListener('beforeunload', () => {\r\n\t\t\tclearApiKeys();\r\n\t\t});\r\n\r\n\t<\/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>Dynamic Conversation Builder Dynamic Conversation Builder Extract keywords, create summaries, and build interactive conversations from your documents Step 1: Input Content Drag &#038; drop a document here or click to select Supported formats: TXT, SRT, JSON, XML, HTML, MD, CSV, ASS, SSA, DOCX, PPTX, PDF Document Information File Name: - File Type: - File Size:...<\/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 3 Conversation 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-3-podcast\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Agent 3 Conversation Creator\" \/>\n<meta property=\"og:description\" content=\"Dynamic Conversation Builder Dynamic Conversation Builder Extract keywords, create summaries, and build interactive conversations from your documents Step\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.hkmu.edu.hk\/oetools\/agent-3-podcast\/\" \/>\n<meta property=\"og:site_name\" content=\"Hong Kong Metropolitan University\" \/>\n<meta property=\"article:modified_time\" content=\"2026-02-20T07:44:01+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 3 Conversation 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-3-podcast\/","og_locale":"en_US","og_type":"article","og_title":"Agent 3 Conversation Creator","og_description":"Dynamic Conversation Builder Dynamic Conversation Builder Extract keywords, create summaries, and build interactive conversations from your documents Step","og_url":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-3-podcast\/","og_site_name":"Hong Kong Metropolitan University","article_modified_time":"2026-02-20T07:44:01+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-3-podcast\/","url":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-3-podcast\/","name":"Agent 3 Conversation Creator - Hong Kong Metropolitan University","isPartOf":{"@id":"https:\/\/www.hkmu.edu.hk\/oetools\/#website"},"datePublished":"2025-06-13T08:28:16+00:00","dateModified":"2026-02-20T07:44:01+00:00","breadcrumb":{"@id":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-3-podcast\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.hkmu.edu.hk\/oetools\/agent-3-podcast\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-3-podcast\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Open Educational Tools","item":"\/oetools\/"},{"@type":"ListItem","position":2,"name":"Agent 3 Conversation 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\/25667"}],"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=25667"}],"version-history":[{"count":238,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages\/25667\/revisions"}],"predecessor-version":[{"id":32239,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages\/25667\/revisions\/32239"}],"wp:attachment":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/media?parent=25667"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}