About

THEO DÕI BLOG ""

Đồng hồ

Người theo dõi

Tổng số lượt xem trang

Bài đăng nổi bật

chạy thử

  <!DOCTYPE html > < html lang = "vi" > < head >     < meta charset = "UTF-8" >     < meta...

Trao đổi, học tập cùng học sinh

Thứ Tư, 30 tháng 7, 2025

chạy thử

 <!DOCTYPE html>

<html lang="vi">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Video Trình Bày Lời Giải</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
    <style>
        body {
            font-family: 'Inter', sans-serif;
        }
        .video-screen {
            background-color: #1a202c; /* bg-gray-900 */
            color: white;
            aspect-ratio: 16 / 9;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            padding: 2rem;
            transition: all 0.5s ease-in-out;
        }
        .question-text {
            font-size: 1.5rem; /* text-2xl */
            font-weight: 600; /* font-semibold */
            opacity: 0;
            transform: translateY(20px);
            transition: opacity 0.5s ease, transform 0.5s ease;
            text-align: center;
        }
        .answer-text {
            margin-top: 1.5rem;
            font-size: 1.25rem; /* text-xl */
            line-height: 1.75;
            min-height: 150px; /* Reserve space for the answer */
            text-align: left;
            width: 100%;
        }
        .answer-text span {
            opacity: 0.3; /* Make upcoming words slightly visible */
            transition: opacity 0.3s ease-in-out, color 0.3s ease-in-out;
        }
        .answer-text span.visible {
            opacity: 1;
            color: #60a5fa; /* A highlight color */
        }
        .spinner {
            border: 4px solid rgba(255, 255, 255, 0.2);
            width: 36px;
            height: 36px;
            border-radius: 50%;
            border-left-color: #3b82f6; /* blue-500 */
            animation: spin 1s ease infinite;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
    </style>
</head>
<body class="bg-gray-200 flex items-center justify-center min-h-screen p-4">
    <div class="w-full max-w-4xl mx-auto">
        <div id="video-container" class="bg-white rounded-xl shadow-2xl overflow-hidden">
            <!-- Video Screen -->
            <div id="display-screen" class="video-screen">
                <div id="question-display" class="question-text"></div>
                <div id="answer-display" class="answer-text"></div>
            </div>

            <!-- Controls -->
            <div class="p-4 bg-gray-100 flex items-center justify-center gap-4">
                <button id="play-button" class="bg-blue-600 text-white font-semibold py-3 px-8 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-transform transform hover:scale-105 flex items-center justify-center gap-2">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
                    Bắt đầu
                </button>
                <div id="loader" class="spinner hidden"></div>
            </div>
        </div>
       
        <!-- Hidden elements -->
        <textarea id="script-input" class="hidden">Câu hỏi: Đội của em làm một loại sản phẩm. Ước tính chi phí để sản xuất x sản phẩm là C(x) = 20x + 60 (nghìn đồng). Nếu tổng số tiền bán hết x sản phẩm là R(x) = 42x (nghìn đồng) thì Đội của em cần bán hết bao nhiêu sản phẩm để số tiền lãi thu được là 380.000 đồng và chi phí cần đầu tư để làm hết x sản phẩm đó là bao nhiêu nghìn đồng?||Đáp án: Số tiền lãi khi bán hết x sản phẩm bằng doanh thu trừ chi phí, tức là số tiền lãi L(x) = R(x) - C(x) = 42x - (20x + 60) = 22x - 60. Theo yêu cầu cần lãi 380.000 đồng nên ta có 22x - 60 = 380. Vậy cần bán hết 20 sản phẩm, chi phí đầu tư ban đầu là C(20) = 20*20 + 60 = 460 nghìn đồng.</textarea>
        <audio id="question-audio-player" class="hidden"></audio>
        <audio id="answer-audio-player" class="hidden"></audio>
        <div id="message-box" class="mt-4 p-4 text-center text-red-700 bg-red-100 rounded-lg hidden"></div>
    </div>

    <script>
        const playButton = document.getElementById('play-button');
        const loader = document.getElementById('loader');
        const questionDisplay = document.getElementById('question-display');
        const answerDisplay = document.getElementById('answer-display');
        const questionAudioPlayer = document.getElementById('question-audio-player');
        const answerAudioPlayer = document.getElementById('answer-audio-player');
        const scriptInput = document.getElementById('script-input');
        const messageBox = document.getElementById('message-box');

        playButton.addEventListener('click', startVideoSimulation);

        function parseScript(fullScript) {
            const parts = fullScript.split('||');
            if (parts.length < 2) {
                return { question: fullScript, answer: "" };
            }
            const question = parts[0].replace('Câu hỏi:', '').trim();
            const answer = parts[1].replace('Đáp án:', '').trim();
            return { question, answer };
        }

        async function startVideoSimulation() {
            // Reset UI
            playButton.disabled = true;
            playButton.classList.add('opacity-50', 'cursor-not-allowed');
            loader.classList.remove('hidden');
            messageBox.classList.add('hidden');
            questionDisplay.style.opacity = '0';
            questionDisplay.style.transform = 'translateY(20px)';
            answerDisplay.innerHTML = '';

            const { question, answer } = parseScript(scriptInput.value);
           
            if (!answer || !question) {
                showMessage("Không tìm thấy câu hỏi hoặc đáp án trong kịch bản.");
                resetControls();
                return;
            }

            try {
                // 1. Generate audio files sequentially to avoid potential auth issues with parallel requests.
                const questionAudioData = await callTtsApi(question);
                const answerAudioData = await callTtsApi(answer);


                if (!questionAudioData || !answerAudioData) {
                    throw new Error("Không thể tạo dữ liệu âm thanh.");
                }

                // 2. Prepare audio players
                const questionAudioUrl = createAudioUrl(questionAudioData);
                const answerAudioUrl = createAudioUrl(answerAudioData);

                questionAudioPlayer.src = questionAudioUrl;
                answerAudioPlayer.src = answerAudioUrl;

                // 3. Chain the playback
                questionAudioPlayer.onloadedmetadata = () => {
                    // Hide loader once the first audio is ready
                    loader.classList.add('hidden');
                   
                    // Display question and play its audio
                    questionDisplay.textContent = `Câu hỏi: ${question}`;
                    questionDisplay.style.opacity = '1';
                    questionDisplay.style.transform = 'translateY(0)';
                    questionAudioPlayer.play();
                };

                questionAudioPlayer.onended = () => {
                    // When question audio ends, start answer animation
                    animateAnswer(answer, answerAudioPlayer);
                };

            } catch (error) {
                console.error('Error in video simulation:', error);
                showMessage("Đã xảy ra lỗi. Vui lòng thử lại.");
                resetControls();
            }
        }

        function animateAnswer(answerText, audioPlayer) {
            const words = answerText.split(/\s+/);
            answerDisplay.innerHTML = words.map(word => `<span>${word} </span>`).join('');
           
            const wordSpans = answerDisplay.querySelectorAll('span');
            if (audioPlayer.duration === 0 || !isFinite(audioPlayer.duration)) {
                 console.warn("Audio duration not available, using fallback timing.");
                 // Fallback if duration is not available
                 audioPlayer.oncanplaythrough = () => audioPlayer.play();
            } else {
                 audioPlayer.play();
            }

            const totalDuration = audioPlayer.duration || (words.length * 0.5); // Use duration or estimate
            const delayPerWord = (totalDuration * 1000) / words.length;

            let wordIndex = 0;
            function revealNextWord() {
                if (wordIndex < wordSpans.length) {
                    wordSpans[wordIndex].classList.add('visible');
                    wordIndex++;
                    setTimeout(revealNextWord, delayPerWord);
                } else {
                    setTimeout(resetControls, 1500); // Re-enable button after animation
                }
            }
           
            setTimeout(revealNextWord, 100); // Start animation
        }
       
        function createAudioUrl(audioData) {
            const sampleRate = 24000;
            const pcmData = base64ToArrayBuffer(audioData);
            const pcm16 = new Int16Array(pcmData);
            const wavBlob = pcmToWav(pcm16, sampleRate);
            return URL.createObjectURL(wavBlob);
        }

        function resetControls() {
            playButton.disabled = false;
            playButton.classList.remove('opacity-50', 'cursor-not-allowed');
            loader.classList.add('hidden');
        }

        function showMessage(message) {
            messageBox.textContent = message;
            messageBox.classList.remove('hidden');
        }

        async function callTtsApi(text) {
            const apiKey = ""; // Provided by environment
            const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent?key=${apiKey}`;
            const payload = {
                contents: [{ parts: [{ text }] }],
                generationConfig: { responseModalities: ["AUDIO"] },
            };
            const response = await fetch(apiUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(payload)
            });
            if (!response.ok) {
                const errorBody = await response.text();
                console.error("API Error Body:", errorBody);
                throw new Error(`API call failed with status: ${response.status}`);
            }
            const result = await response.json();
            return result?.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
        }

        // --- Audio Helper Functions ---
        function base64ToArrayBuffer(base64) {
            const binaryString = window.atob(base64);
            const len = binaryString.length;
            const bytes = new Uint8Array(len);
            for (let i = 0; i < len; i++) {
                bytes[i] = binaryString.charCodeAt(i);
            }
            return bytes.buffer;
        }
        function pcmToWav(pcmData, sampleRate) {
            const numChannels = 1, bitsPerSample = 16;
            const byteRate = sampleRate * numChannels * (bitsPerSample / 8);
            const blockAlign = numChannels * (bitsPerSample / 8);
            const dataSize = pcmData.length * (bitsPerSample / 8);
            const buffer = new ArrayBuffer(44 + dataSize);
            const view = new DataView(buffer);
            writeString(view, 0, 'RIFF'); view.setUint32(4, 36 + dataSize, true); writeString(view, 8, 'WAVE');
            writeString(view, 12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true);
            view.setUint16(22, numChannels, true); view.setUint32(24, sampleRate, true);
            view.setUint32(28, byteRate, true); view.setUint16(32, blockAlign, true);
            view.setUint16(34, bitsPerSample, true);
            writeString(view, 36, 'data'); view.setUint32(40, dataSize, true);
            for (let i = 0; i < pcmData.length; i++) {
                view.setInt16(44 + i * 2, pcmData[i], true);
            }
            return new Blob([view], { type: 'audio/wav' });
        }
        function writeString(view, offset, string) {
            for (let i = 0; i < string.length; i++) {
                view.setUint8(offset + i, string.charCodeAt(i));
            }
        }
    </script>
</body>
</html>

0 nhận xét:

Đăng nhận xét