junkerstock
 代読テスト08 

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Voice Changer UI</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic ProN", sans-serif;
background: #f2f2f7;
margin: 0;
padding: 0;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}

/* --- 上部 (33%) --- */
.top-section {
height: 33%;
padding: 15px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
background: #fff;
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
z-index: 10;
}

#output {
flex-grow: 1;
font-size: 18px;
font-weight: bold;
color: #333;
padding: 5px;
overflow-y: auto;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
margin-bottom: 10px;
}

#recBtn {
width: 100%;
height: 55px;
background-color: #ff3b30;
color: white;
font-size: 18px;
font-weight: bold;
border: none;
border-radius: 28px;
box-shadow: 0 3px 8px rgba(255, 59, 48, 0.4);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
flex-shrink: 0;
transition: transform 0.1s;
}
#recBtn:active { transform: scale(0.95); }
.pulsing { animation: pulse 1.5s infinite; }
@keyframes pulse {
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 59, 48, 0.7); }
70% { transform: scale(1.02); box-shadow: 0 0 0 8px rgba(255, 59, 48, 0); }
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 59, 48, 0); }
}

/* --- 下部 (67%) --- */
.bottom-section {
height: 67%;
padding: 15px;
padding-bottom: calc(15px + env(safe-area-inset-bottom));
box-sizing: border-box;
background: #f2f2f7;

display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
align-content: start; /* 上詰め */
overflow-y: auto;
}

.voice-btn {
width: 100%;
height: 45px; /* ★修正:高さを固定して低くしました(前回はアスペクト比で高くなっていました) */
font-size: 13px;
font-weight: bold;
color: white;
border: none;
border-radius: 10px; /* 角丸も少し控えめに */
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 0 5px;
word-break: break-all;
-webkit-tap-highlight-color: transparent;
}
.voice-btn:active { transform: scale(0.92); }

.msg { width: 100%; text-align: center; color: #888; grid-column: span 3; margin-top: 20px;}
</style>
</head>
<body>

<div class="top-section">
<div id="output">マイクを押して話す</div>
<button id="recBtn">
<span>●</span> 録音
</button>
</div>

<div class="bottom-section" id="voiceContainer">
<div class="msg">声を読み込み中...</div>
</div>

<script>
const recBtn = document.getElementById('recBtn');
const output = document.getElementById('output');
const voiceContainer = document.getElementById('voiceContainer');

let availableVoices = [];
let recognizedText = "";

const colors = [
'#5856D6', '#007AFF', '#34C759', '#FF9500',
'#AF52DE', '#FF2D55', '#5AC8FA', '#FFCC00',
'#8E8E93', '#4CD964', '#FF3B30', '#00C7BE'
];

function loadVoices() {
const allVoices = window.speechSynthesis.getVoices();
if (allVoices.length === 0) return;

const seenNames = new Set();

// 重複を除去しつつ日本語を取得
availableVoices = allVoices.filter(v => {
if (v.lang !== 'ja-JP') return false;
if (seenNames.has(v.name)) return false;
seenNames.add(v.name);
return true;
});

voiceContainer.innerHTML = '';
if (availableVoices.length === 0) {
voiceContainer.innerHTML = '<div class="msg">日本語の声がありません</div>';
return;
}

availableVoices.forEach((voice, index) => {
const btn = document.createElement('button');
btn.className = 'voice-btn';
btn.style.backgroundColor = colors[index % colors.length];

let label = voice.name
.replace('O-ren', 'Oren')
.replace('Siri', '')
.replace('Enhanced', '')
.trim()
.split(' ')[0];

btn.innerText = label || "Voice";
btn.onclick = () => speakText(index);
voiceContainer.appendChild(btn);
});
}

// iOS Safariでの読み込み対策
window.speechSynthesis.onvoiceschanged = loadVoices;
loadVoices();

function speakText(voiceIndex) {
if (!recognizedText) {
output.innerText = "録音してからボタンを押してね";
output.style.color = "#ccc";
return;
}

window.speechSynthesis.cancel();
const uttr = new SpeechSynthesisUtterance(recognizedText);
uttr.voice = availableVoices[voiceIndex];
uttr.pitch = 1.0;
uttr.rate = 1.0;
window.speechSynthesis.speak(uttr);
}

const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;

if (!SpeechRecognition) {
output.innerText = "Safariで開いてください";
recBtn.disabled = true;
} else {
const recognition = new SpeechRecognition();
recognition.lang = 'ja-JP';
recognition.interimResults = false;

recBtn.onclick = () => {
window.speechSynthesis.cancel();
try {
recognition.start();
recBtn.innerHTML = '<span>■</span> 停止';
recBtn.classList.add('pulsing');
output.style.color = "#888";
} catch(e) {}
};

recognition.onresult = (event) => {
recognizedText = event.results[0][0].transcript;
output.innerText = recognizedText;
output.style.color = "#000";
resetRecBtn();
};

recognition.onend = resetRecBtn;
recognition.onerror = resetRecBtn;

function resetRecBtn() {
recBtn.innerHTML = '<span>●</span> 録音';
recBtn.classList.remove('pulsing');
}
}
</script>

</body>
</html>


使用変数

allVoices
availableVoices
backgroundColor
btn
charset
class
className
color
colors
content
disabled
fit
id
innerHTML
innerText
interimResults
label
lang
length
loadVoices -------( Function )
name
onclick
onend
onerror
onresult
onvoiceschanged
output
pitch
rate
recBtn
recognition
recognizedText
resetRecBtn -------( Function )
scalable
scale
seenNames
speakText -------( Function )
SpeechRecognition
uttr
v
voice
voiceContainer
width