<!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%; /* 画面の3分の1 */
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;
transition: transform 0.1s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
flex-shrink: 0; /* 縮まないように固定 */
}
#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%; /* 画面の3分の2 */
padding: 15px;
padding-bottom: calc(15px + env(safe-area-inset-bottom)); /* iPhone下のバー対策 */
box-sizing: border-box;
/* グリッドレイアウト:3列にしてボタンを小さく */
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px; /* ボタン同士の隙間 */
align-content: start; /* 上詰め配置 */
overflow-y: auto; /* ボタンが多い時はここだけスクロール */
}
.voice-btn {
width: 100%;
aspect-ratio: 1.3 / 1; /* 横長比率を固定 */
min-height: 50px; /* 最低限の高さ */
font-size: 14px; /* 文字サイズ調整 */
font-weight: bold;
color: white;
border: none;
border-radius: 12px;
box-shadow: 0 2px 5px rgba(0,0,0,0.15);
cursor: pointer;
transition: transform 0.1s;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 5px;
-webkit-tap-highlight-color: transparent;
word-break: break-all; /* 長い名前対策 */
}
.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',
'#4CD964', '#FF3B30', '#8E8E93', '#00C7BE'
];
function loadVoices() {
const allVoices = window.speechSynthesis.getVoices();
if (allVoices.length === 0) return;
availableVoices = allVoices.filter(v => v.lang === 'ja-JP');
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];
// 名前を整形(O-ren -> Oren, Siri等の余計な文字を削除)
let label = voice.name.replace('O-ren', 'Oren').replace('Siri', '').trim().split(' ')[0];
// シンプルに名前だけ表示
btn.innerText = label || voice.name.slice(0, 6);
btn.onclick = () => speakText(index);
voiceContainer.appendChild(btn);
});
}
window.speechSynthesis.onvoiceschanged = loadVoices;
loadVoices();
function speakText(voiceIndex) {
if (!recognizedText) {
// テスト用
const dummy = "録音してからボタンを押してください";
output.innerText = dummy;
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 = "非対応ブラウザ";
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 | |
| dummy | |
| 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 | |
| speakText -------( Function ) | |
| SpeechRecognition | |
| uttr | |
| v | |
| voice | |
| voiceContainer | |
| width |