junkerstock
 代読テスト05 

<!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;
}

/* --- 上半分 (45%):表示と録音 --- */
.top-section {
height: 45%;
padding: 20px;
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: 22px;
font-weight: bold;
color: #333;
padding: 10px;
overflow-y: auto; /* 文字数が多い時だけスクロール */
display: flex;
align-items: center; /* 文字を垂直中央に */
justify-content: center;
text-align: center;
}

#recBtn {
width: 100%;
height: 70px; /* 押しやすい高さ */
background-color: #ff3b30; /* iPhoneらしい赤 */
color: white;
font-size: 20px;
font-weight: bold;
border: none;
border-radius: 35px; /* 丸み */
box-shadow: 0 4px 10px rgba(255, 59, 48, 0.4);
cursor: pointer;
transition: transform 0.1s, opacity 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
#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 10px rgba(255, 59, 48, 0); }
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 59, 48, 0); }
}

/* --- 下半分 (55%):再生ボタン群 --- */
.bottom-section {
height: 55%;
padding: 20px;
/* iPhoneの下部バー(セーフエリア)対策の余白 */
padding-bottom: calc(20px + env(safe-area-inset-bottom));
box-sizing: border-box;

/* グリッドレイアウトでボタンを並べる */
display: grid;
grid-template-columns: 1fr 1fr; /* 2列 */
gap: 15px;
align-content: center; /* 垂直方向中央寄せ */
overflow-y: auto; /* ボタンが多すぎる場合のみスクロール許可 */
}

.voice-btn {
width: 100%;
height: 100%;
min-height: 80px; /* ボタンの最低の高さ */
font-size: 18px;
font-weight: bold;
color: white;
border: none;
border-radius: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.15);
cursor: pointer;
transition: transform 0.1s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
-webkit-tap-highlight-color: transparent;
}
.voice-btn:active { transform: scale(0.95); }

/* 声の名前の下に小さく補足を入れる用 */
.voice-sub { font-size: 12px; opacity: 0.8; font-weight: normal; }

/* メッセージ表示用 */
.msg { width: 100%; text-align: center; color: #888; grid-column: span 2; }

</style>
</head>
<body>

<div class="top-section">
<div id="output">マイクボタンを押して<br>話しかけてください</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' // 黄色
];

// 声を読み込んでボタンを作る関数
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];

// ボタンの文字(名前を短く表示)
// 例: "Kyoko" だけ表示させたいが、場合によっては "Kyoko (Premium)" とかなのでそのまま出す
let label = voice.name.replace('O-ren', 'Oren').split(' ')[0]; // スペースより前だけ取ってみる(簡易短縮)

btn.innerHTML = `
<span>${label}</span>
<span class="voice-sub">で再生</span>
`;

// ボタンを押した時の動作(再生)
btn.onclick = () => speakText(index);

voiceContainer.appendChild(btn);
});
}

// iOS用の読み込みトリガー
window.speechSynthesis.onvoiceschanged = loadVoices;
loadVoices();

// 再生処理
function speakText(voiceIndex) {
if (!recognizedText) {
// 文字がない場合のテスト用
alert("先に上のボタンで録音してください");
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) {
console.log("start error", e);
}
};

recognition.onresult = (event) => {
recognizedText = event.results[0][0].transcript;
output.innerText = recognizedText;
output.style.color = "#000"; // 確定したら黒く

resetRecBtn();
};

recognition.onend = () => resetRecBtn();
recognition.onerror = () => {
output.innerText = "エラー:もう一度押してください";
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
speakText -------( Function )
SpeechRecognition
uttr
v
voice
voiceContainer
width