<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>阿卡苍蝇的奇妙问答</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
@import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600&family=ZCOOL+KuaiLe&display=swap');
body {
background-color: #fdf2f8;
font-family: 'Fredoka', 'ZCOOL KuaiLe', sans-serif;
overflow-x: hidden;
touch-action: manipulation;
}
.btn-press {
transition: transform 0.1s;
}
.btn-press:active {
transform: scale(0.95);
}
/* 录音时的波纹动画 */
.listening-ring {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
border-radius: 50%;
border: 4px solid rgba(139, 92, 246, 0.6);
animation: ripple 1.5s infinite;
z-index: -1;
pointer-events: none;
}
@keyframes ripple {
0% { width: 100%; height: 100%; opacity: 0.8; }
100% { width: 180%; height: 180%; opacity: 0; }
}
.mascot-bounce {
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-15px) rotate(5deg); }
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* 视频播放器样式 */
.video-overlay {
background: linear-gradient(rgba(0,0,0,0.1), rgba(0,0,0,0.4));
}
.ken-burns {
animation: kenBurns 10s infinite alternate;
}
@keyframes kenBurns {
0% { transform: scale(1); }
100% { transform: scale(1.1); }
}
.dot-flashing {
position: relative;
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #8b5cf6;
color: #8b5cf6;
animation: dot-flashing 1s infinite linear alternate;
animation-delay: 0.5s;
}
.dot-flashing::before, .dot-flashing::after {
content: '';
display: inline-block;
position: absolute;
top: 0;
}
.dot-flashing::before {
left: -15px;
width: 10px; height: 10px; border-radius: 5px; background-color: #8b5cf6; color: #8b5cf6;
animation: dot-flashing 1s infinite alternate;
animation-delay: 0s;
}
.dot-flashing::after {
left: 15px;
width: 10px; height: 10px; border-radius: 5px; background-color: #8b5cf6; color: #8b5cf6;
animation: dot-flashing 1s infinite alternate;
animation-delay: 1s;
}
@keyframes dot-flashing {
0% { background-color: #8b5cf6; }
50%, 100% { background-color: #ddd6fe; }
}
</style>
</head>
<body class="h-screen flex flex-col items-center justify-center bg-purple-50">
<!-- 主容器 -->
<div class="w-full h-full md:w-[400px] md:h-[800px] bg-white md:rounded-[3rem] shadow-2xl overflow-hidden flex flex-col relative border-8 border-purple-200">
<!-- 顶部:标题栏 -->
<header class="bg-purple-600 p-4 pt-6 text-center shadow-sm z-10 rounded-b-3xl relative">
<h1 class="text-2xl text-white font-bold tracking-wider">
<i class="fas fa-glasses text-yellow-300 mr-2"></i>阿卡苍蝇
<span class="block text-xs font-normal text-purple-200 mt-1">Aka Fly's Answers</span>
</h1>
<div class="absolute top-2 right-4 text-xs text-purple-300 opacity-80">🪰 嗡嗡~</div>
</header>
<!-- 中间:内容展示区 -->
<main id="chat-container" class="flex-1 overflow-y-auto p-4 no-scrollbar flex flex-col space-y-4 pb-32">
<!-- 欢迎消息 -->
<div class="flex flex-col items-center mt-4 mb-8">
<div class="text-7xl mb-2 mascot-bounce filter drop-shadow-lg">🪰</div>
<div class="bg-purple-100 text-purple-900 p-4 rounded-2xl rounded-tl-none shadow-sm max-w-[90%] border-2 border-purple-200">
<p class="text-lg font-bold">嗨!我是阿卡!👓</p>
<p class="text-base mt-2">我和你一样,对这个世界充满了好奇!你想知道什么?我可以用魔法视频告诉你哦,嗡嗡!</p>
</div>
</div>
<!-- 用户问题 -->
<div id="user-question-area" class="hidden flex justify-end">
<div class="bg-green-100 text-green-900 p-3 rounded-2xl rounded-tr-none max-w-[85%] shadow-sm">
<p id="user-text" class="font-bold text-lg">...</p>
</div>
</div>
<!-- AI 回答 -->
<div id="ai-answer-area" class="hidden flex flex-col items-start space-y-2">
<div class="text-4xl transform -scale-x-100">🪰</div>
<div class="bg-white border-2 border-purple-300 text-gray-800 p-4 rounded-2xl rounded-tl-none shadow-md w-full relative">
<!-- 答案文本 -->
<p id="answer-text" class="text-lg leading-relaxed mb-3 font-medium">...</p>
<!-- 视频/幻灯片展示区 -->
<div id="video-container" class="hidden mt-3 rounded-xl overflow-hidden relative group cursor-pointer border-2 border-gray-200 bg-black aspect-video">
<!-- 图片容器 -->
<img id="video-slide" src="" alt="Video Frame" class="w-full h-full object-cover opacity-0 transition-opacity duration-500">
<!-- 播放按钮遮罩 -->
<div id="play-overlay" class="absolute inset-0 flex items-center justify-center video-overlay z-10" onclick="playVideoSequence()">
<div class="w-14 h-14 bg-white/90 rounded-full flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform cursor-pointer">
<i class="fas fa-play text-red-500 text-2xl ml-1"></i>
</div>
</div>
<!-- 播放中状态栏 -->
<div id="video-controls" class="absolute bottom-0 left-0 right-0 bg-black/60 text-white p-2 flex justify-between items-center text-xs opacity-0 transition-opacity">
<span><i class="fas fa-video mr-1"></i> 视频解答</span>
<span id="video-timer">00:00 / 00:10</span>
</div>
<!-- 加载中 -->
<div id="video-loading" class="absolute inset-0 flex flex-col items-center justify-center text-white text-sm z-20 bg-gray-900">
<i class="fas fa-film animate-spin text-2xl mb-2 text-purple-400"></i>
<span>阿卡正在生成视频...</span>
<span class="text-xs text-gray-400 mt-1">(搜索网络素材中)</span>
</div>
</div>
<!-- 操作栏 -->
<div class="flex flex-wrap justify-between items-center mt-3 pt-3 border-t border-gray-100 gap-2">
<!-- Gemini Magic Voice Button (Only Audio) -->
<button id="magic-voice-btn" onclick="playGeminiTTS()" class="flex-1 bg-gray-100 text-gray-600 hover:bg-gray-200 px-3 py-2 rounded-xl text-sm font-bold transition flex items-center justify-center">
<i class="fas fa-headphones-alt mr-2"></i> 听阿卡说
</button>
<!-- Video Generation Button -->
<button id="generate-video-btn" onclick="initVideo()" class="flex-[1.5] bg-gradient-to-r from-red-500 to-pink-600 text-white hover:from-red-600 hover:to-pink-700 px-3 py-2 rounded-xl text-sm font-bold transition flex items-center justify-center shadow-md border border-red-400">
<i class="fas fa-play-circle mr-2"></i> 生成视频解答
</button>
</div>
</div>
</div>
<!-- 加载中动画 -->
<div id="loading-indicator" class="hidden text-center py-6 w-full flex justify-center flex-col items-center">
<div class="text-3xl mb-2 animate-bounce">👓</div>
<div class="dot-flashing"></div>
<p class="text-xs text-purple-400 mt-2">阿卡正在思考...嗡嗡...</p>
</div>
<div id="error-message" class="hidden text-center text-red-400 text-sm py-2">
哎呀,信号不好,请再说一次!
</div>
</main>
<!-- 底部:控制区 -->
<footer class="bg-white p-4 pb-6 border-t border-purple-100 z-20 flex flex-col items-center relative shadow-[0_-5px_15px_rgba(0,0,0,0.05)]">
<!-- 输入框和发送按钮 -->
<div class="w-full flex items-center space-x-2 mb-4">
<div class="flex-1 bg-gray-100 rounded-full px-4 py-2 flex items-center focus-within:ring-2 focus-within:ring-purple-300 transition">
<i class="fas fa-keyboard text-gray-400 mr-2"></i>
<input type="text" id="text-input" placeholder="问问阿卡..."
class="bg-transparent border-none outline-none w-full text-gray-700 placeholder-gray-400"
onkeypress="handleKeyPress(event)">
</div>
<button onclick="processInput()" class="bg-purple-500 hover:bg-purple-600 text-white w-10 h-10 rounded-full flex items-center justify-center shadow-sm transition btn-press">
<i class="fas fa-paper-plane"></i>
</button>
</div>
<!-- 语音按钮 -->
<div class="relative group">
<div id="mic-ring" class="hidden listening-ring"></div>
<button id="mic-btn" onclick="toggleSpeech()"
class="btn-press w-20 h-20 bg-gradient-to-br from-purple-500 to-indigo-600 rounded-full shadow-xl shadow-purple-300 flex items-center justify-center text-white relative outline-none select-none transition-all duration-300 z-10 border-4 border-white">
<i id="mic-icon" class="fas fa-microphone text-3xl"></i>
</button>
</div>
<!-- 状态提示 -->
<div id="status-bar" class="text-center text-gray-400 text-xs mt-3 h-4 font-bold">
点一下,开始问问题!
</div>
</footer>
</div>
<script>
// --- 全局变量与配置 ---
const apiKey = ""; // API Key injected by environment
const GENERATE_MODEL = "gemini-2.5-flash-preview-09-2025";
const TTS_MODEL = "gemini-2.5-flash-preview-tts";
// DOM 元素
const textInput = document.getElementById('text-input');
const userArea = document.getElementById('user-question-area');
const userText = document.getElementById('user-text');
const aiArea = document.getElementById('ai-answer-area');
const answerText = document.getElementById('answer-text');
const videoContainer = document.getElementById('video-container');
const videoSlide = document.getElementById('video-slide');
const videoLoading = document.getElementById('video-loading');
const playOverlay = document.getElementById('play-overlay');
const videoControls = document.getElementById('video-controls');
const loading = document.getElementById('loading-indicator');
const errorMsg = document.getElementById('error-message');
const statusText = document.getElementById('status-bar');
const micBtn = document.getElementById('mic-btn');
const micIcon = document.getElementById('mic-icon');
const micRing = document.getElementById('mic-ring');
const magicVoiceBtn = document.getElementById('magic-voice-btn');
let isListening = false;
let synthesis = window.speechSynthesis;
let recognition;
let lastAnswerText = "";
let currentAudio = null;
// 视频相关变量
let videoFrames = []; // 存储生成的图片URL
let currentFrameIndex = 0;
let slideInterval = null;
let videoKeywords = [];
// --- 核心逻辑: Gemini API 调用 ---
async function fetchGeminiAnswer(userQuery) {
const url = `https://generativelanguage.googleapis.com/v1beta/models/${GENERATE_MODEL}:generateContent?key=${apiKey}`;
// 系统提示词:平辈、搞怪、朋友、视频素材
const systemPrompt = `
You are '阿卡' (Aka), a funny, curious, and friendly fly detective.
You are a friend/peer to the 6-9 year old user.
NEVER use "本大爷" or arrogant titles. Use "我" or "我们" or "阿卡".
Tone: Playful, excited, peer-to-peer, slightly mischievous.
End sentences with "嗡嗡" occasionally.
IMPORTANT: Output ONLY valid JSON with this schema:
{
"answer": "The text answer in Chinese. Keep it under 4 sentences. Make it sound like a friend explaining a cool secret.",
"video_scenes": [
"Description of scene 1 (English) for video generation",
"Description of scene 2 (English) for video generation",
"Description of scene 3 (English) for video generation"
],
"language": "The language code ('zh-CN' or 'en-US')."
}`;
const payload = {
contents: [{ parts: [{ text: userQuery }] }],
systemInstruction: { parts: [{ text: systemPrompt }] },
generationConfig: { responseMimeType: "application/json" }
};
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) throw new Error('API Error');
const data = await response.json();
return JSON.parse(data.candidates[0].content.parts[0].text);
} catch (error) {
console.error("Gemini Gen Error:", error);
throw error;
}
}
async function fetchGeminiTTS(text, langCode) {
const url = `https://generativelanguage.googleapis.com/v1beta/models/${TTS_MODEL}:generateContent?key=${apiKey}`;
// 使用男声,提示词去掉 dramatic,改为 funny friend
const voiceName = 'Fenrir';
const promptText = `Say in a funny, friendly, slightly mischievous male cartoon voice (like a peer): ${text}`;
const payload = {
contents: [{ parts: [{ text: promptText }] }],
generationConfig: {
responseModalities: ["AUDIO"],
speechConfig: {
voiceConfig: {
prebuiltVoiceConfig: { voiceName: voiceName }
}
}
}
};
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) throw new Error('TTS API Error');
const data = await response.json();
return data.candidates[0].content.parts[0].inlineData.data;
} catch (error) {
console.error("Gemini TTS Error:", error);
throw error;
}
}
// --- 音频处理工具 ---
function base64ToWav(base64Data) {
const binaryString = window.atob(base64Data);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return pcmToWav(bytes, 24000, 1, 16);
}
function pcmToWav(pcmData, sampleRate, numChannels, bitsPerSample) {
const header = new ArrayBuffer(44);
const view = new DataView(header);
const dataSize = pcmData.length;
const fileSize = 36 + dataSize;
writeString(view, 0, 'RIFF');
view.setUint32(4, fileSize, 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, sampleRate * numChannels * (bitsPerSample / 8), true);
view.setUint16(32, numChannels * (bitsPerSample / 8), true);
view.setUint16(34, bitsPerSample, true);
writeString(view, 36, 'data');
view.setUint32(40, dataSize, true);
const blob = new Blob([header, pcmData], { type: 'audio/wav' });
return URL.createObjectURL(blob);
}
function writeString(view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
// --- 主交互流程 ---
async function processInput() {
const question = textInput.value.trim();
if (!question) return;
// UI Reset
stopAudio();
stopVideo();
textInput.value = '';
textInput.blur();
userText.innerText = question;
userArea.classList.remove('hidden');
aiArea.classList.add('hidden');
loading.classList.remove('hidden');
errorMsg.classList.add('hidden');
videoContainer.classList.add('hidden');
statusText.innerText = "阿卡正在思考...嗡嗡...";
scrollToBottom();
try {
const result = await fetchGeminiAnswer(question);
lastAnswerText = result.answer;
videoKeywords = result.video_scenes || ["cartoon illustration"];
const lang = result.language || 'zh-CN';
loading.classList.add('hidden');
aiArea.classList.remove('hidden');
answerText.innerHTML = highlightKeywords(lastAnswerText);
// 默认不直接播声音,等待用户点击“生成视频”或“听阿卡说”
// playStandardTTS(lang); // Optional: Play simple TTS immediately? Maybe not for video feel.
statusText.innerText = "解答来了!点击按钮生成视频!";
scrollToBottom();
} catch (err) {
loading.classList.add('hidden');
errorMsg.classList.remove('hidden');
statusText.innerText = "哎呀,信号不好,重试一下?";
}
}
// --- 视频生成与播放逻辑 ---
function initVideo() {
if (!videoKeywords || videoKeywords.length === 0) return;
videoContainer.classList.remove('hidden');
videoLoading.style.display = 'flex'; // 显示加载中
playOverlay.style.display = 'none';
videoControls.style.opacity = '0';
videoSlide.style.opacity = '0';
scrollToBottom();
videoFrames = [];
let loadedCount = 0;
const totalFrames = videoKeywords.length;
// 并行预加载所有生成的图片帧
videoKeywords.forEach((desc, index) => {
const seed = Math.floor(Math.random() * 1000);
// 统一风格
const prompt = encodeURIComponent(`${desc}, cartoon style, vibrant colors, vector illustration for kids, simple`);
const url = `https://image.pollinations.ai/prompt/${prompt}?width=600&height=340&nologo=true&seed=${seed}`;
const img = new Image();
img.onload = () => {
loadedCount++;
if (loadedCount >= totalFrames) {
finishVideoLoading();
}
};
img.src = url;
videoFrames.push(url);
});
// 如果加载太慢,3秒后强制显示已加载的
setTimeout(() => {
if (videoLoading.style.display !== 'none') finishVideoLoading();
}, 3500);
}
function finishVideoLoading() {
videoLoading.style.display = 'none';
playOverlay.style.display = 'flex'; // 显示播放按钮
// 设置第一帧封面
if (videoFrames.length > 0) {
videoSlide.src = videoFrames[0];
videoSlide.style.opacity = '0.7'; // 稍微暗一点显示封面
}
statusText.innerText = "视频准备好了!点击播放 ▶️";
}
// 播放 "视频" (图片轮播 + 语音)
async function playVideoSequence() {
stopAudio();
playOverlay.style.display = 'none';
videoControls.style.opacity = '1';
videoSlide.style.opacity = '1';
videoSlide.classList.add('ken-burns'); // 添加缓慢放大效果
// 1. 开始生成并播放语音
const btn = document.getElementById('generate-video-btn');
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 加载语音...';
try {
// 如果没有缓存音频,则请求
if (!currentAudio) {
const langCode = /[^\x00-\xff]/.test(lastAnswerText) ? 'zh-CN' : 'en-US';
const pcmBase64 = await fetchGeminiTTS(lastAnswerText, langCode);
const wavUrl = base64ToWav(pcmBase64);
currentAudio = new Audio(wavUrl);
}
// 2. 播放音频
currentAudio.play();
statusText.innerText = "正在播放视频解答...";
// 3. 开始图片轮播 (根据音频时长估算,或者固定间隔)
currentFrameIndex = 0;
videoSlide.src = videoFrames[0];
// 每3秒换一张图,模拟分镜切换
if (slideInterval) clearInterval(slideInterval);
slideInterval = setInterval(() => {
currentFrameIndex = (currentFrameIndex + 1) % videoFrames.length;
videoSlide.src = videoFrames[currentFrameIndex];
// 重置动画以产生切换感
videoSlide.classList.remove('ken-burns');
void videoSlide.offsetWidth; // trigger reflow
videoSlide.classList.add('ken-burns');
}, 3000);
// 音频结束后重置
currentAudio.onended = () => {
stopVideo();
statusText.innerText = "播放结束!";
};
} catch (e) {
console.error(e);
alert("播放出错啦");
stopVideo();
} finally {
btn.innerHTML = originalText;
}
}
function stopVideo() {
if (slideInterval) clearInterval(slideInterval);
if (currentAudio) {
currentAudio.pause();
currentAudio.currentTime = 0;
}
videoSlide.classList.remove('ken-burns');
playOverlay.style.display = 'flex';
videoControls.style.opacity = '0';
}
// 单独播放语音 (不带视频)
async function playGeminiTTS() {
stopAudio();
if (!lastAnswerText) return;
const btn = magicVoiceBtn;
const originalHtml = btn.innerHTML;
try {
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> ...';
btn.classList.add('opacity-75');
const langCode = /[^\x00-\xff]/.test(lastAnswerText) ? 'zh-CN' : 'en-US';
const pcmBase64 = await fetchGeminiTTS(lastAnswerText, langCode);
const wavUrl = base64ToWav(pcmBase64);
// 这里创建一个新的 Audio 对象用于纯音频播放,不干扰视频逻辑
const audio = new Audio(wavUrl);
audio.play();
} catch (e) {
alert("阿卡没听清,再试一次");
} finally {
btn.innerHTML = originalHtml;
btn.classList.remove('opacity-75');
}
}
function stopAudio() {
if (synthesis) synthesis.cancel();
if (currentAudio) {
currentAudio.pause();
// 不清空 currentAudio 以便视频重播
}
}
function scrollToBottom() {
setTimeout(() => {
const container = document.getElementById('chat-container');
container.scrollTop = container.scrollHeight;
}, 100);
}
function highlightKeywords(text) {
return text.replace(/(因为|所以|嗡嗡|阿卡|我们)/g, '<span class="text-purple-600 font-bold">$1</span>');
}
function handleKeyPress(e) {
if (e.key === 'Enter') processInput();
}
window.speechSynthesis.onvoiceschanged = function() {
window.speechSynthesis.getVoices();
};
// --- 语音识别 (Toggle) ---
if ('webkitSpeechRecognition' in window) {
recognition = new webkitSpeechRecognition();
recognition.continuous = false;
recognition.interimResults = false;
recognition.lang = 'zh-CN';
recognition.onstart = function() {
isListening = true;
updateMicUI(true);
textInput.placeholder = "正在听...嗡嗡...";
statusText.innerText = "请说... Listening...";
};
recognition.onend = function() {
isListening = false;
updateMicUI(false);
textInput.placeholder = "问问阿卡...";
statusText.innerText = "点一下,问阿卡问题!";
};
recognition.onresult = function(event) {
const transcript = event.results[0][0].transcript;
if (transcript && transcript.trim().length > 0) {
textInput.value = transcript;
statusText.innerText = "收到!Thinking...";
setTimeout(processInput, 500);
}
};
} else {
micBtn.style.display = 'none';
}
function toggleSpeech() {
stopAudio();
if (!recognition) return;
if (isListening) recognition.stop();
else try { recognition.start(); } catch (e) { recognition.stop(); }
}
function updateMicUI(listening) {
if (listening) {
micBtn.classList.remove('from-purple-500', 'to-indigo-600');
micBtn.classList.add('bg-red-500', 'animate-pulse');
micBtn.style.background = '#ef4444';
micIcon.classList.remove('fa-microphone');
micIcon.classList.add('fa-stop');
micRing.classList.remove('hidden');
} else {
micBtn.style.background = '';
micBtn.classList.add('from-purple-500', 'to-indigo-600');
micBtn.classList.remove('bg-red-500', 'animate-pulse');
micIcon.classList.add('fa-microphone');
micIcon.classList.remove('fa-stop');
micRing.classList.add('hidden');
}
}
</script>
</body>
</html>