junkerstock
 代読テスト06 

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

Content-type: text/html error-smemo8

ERROR !

ファイルの差し替えに失敗しました: ./smemo8.log