Upload files to "/"
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Navbar, NavTab, AgeGroup } from './components/Navbar';
|
||||
import { StoryReader } from './components/StoryReader';
|
||||
import { StoryboardView } from './components/StoryboardView';
|
||||
import { ArtDirectionView } from './components/ArtDirectionView';
|
||||
import { CharacterVault } from './components/CharacterVault';
|
||||
import { AdaptabilityView } from './components/AdaptabilityView';
|
||||
import { FormatStrategyView } from './components/FormatStrategyView';
|
||||
import { ActivityCorner } from './components/ActivityCorner';
|
||||
import { PitchDeckModal } from './components/PitchDeckModal';
|
||||
import { BOOK_METADATA } from './data/storyData';
|
||||
import { soundManager } from './utils/audio';
|
||||
import { Heart } from 'lucide-react';
|
||||
|
||||
export function App() {
|
||||
const [activeTab, setActiveTab] = useState<NavTab>('reader');
|
||||
const [ageGroup, setAgeGroup] = useState<AgeGroup>('pictureBook');
|
||||
const [isBedtimeMode, setIsBedtimeMode] = useState<boolean>(false);
|
||||
const [soundEnabled, setSoundEnabled] = useState<boolean>(true);
|
||||
const [isPitchDeckOpen, setIsPitchDeckOpen] = useState<boolean>(false);
|
||||
const [storyboardSceneId, setStoryboardSceneId] = useState<number>(1);
|
||||
|
||||
// Sync global .dark class on <html> with Bedtime Mode so all dark: variants apply
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (isBedtimeMode) {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
}, [isBedtimeMode]);
|
||||
|
||||
const handleExploreStoryboard = (sceneId: number) => {
|
||||
soundManager.playPop();
|
||||
setStoryboardSceneId(sceneId);
|
||||
setActiveTab('storyboard');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`min-h-screen flex flex-col font-reading transition-colors duration-500 ${
|
||||
isBedtimeMode
|
||||
? 'bg-slate-950 text-indigo-50'
|
||||
: 'bg-[#faf6ee] text-stone-900'
|
||||
}`}>
|
||||
{/* Top Navigation */}
|
||||
<Navbar
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
ageGroup={ageGroup}
|
||||
setAgeGroup={setAgeGroup}
|
||||
isBedtimeMode={isBedtimeMode}
|
||||
setIsBedtimeMode={setIsBedtimeMode}
|
||||
soundEnabled={soundEnabled}
|
||||
setSoundEnabled={setSoundEnabled}
|
||||
onOpenPitchDeck={() => setIsPitchDeckOpen(true)}
|
||||
/>
|
||||
|
||||
{/* Main View Router */}
|
||||
<main className="flex-1">
|
||||
{activeTab === 'reader' && (
|
||||
<StoryReader
|
||||
ageGroup={ageGroup}
|
||||
setAgeGroup={setAgeGroup}
|
||||
isBedtimeMode={isBedtimeMode}
|
||||
soundEnabled={soundEnabled}
|
||||
onExploreStoryboard={handleExploreStoryboard}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'storyboard' && (
|
||||
<StoryboardView
|
||||
isBedtimeMode={isBedtimeMode}
|
||||
selectedSceneId={storyboardSceneId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'art' && (
|
||||
<ArtDirectionView
|
||||
isBedtimeMode={isBedtimeMode}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'characters' && (
|
||||
<CharacterVault
|
||||
isBedtimeMode={isBedtimeMode}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'adaptability' && (
|
||||
<AdaptabilityView
|
||||
isBedtimeMode={isBedtimeMode}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'publishing' && (
|
||||
<FormatStrategyView
|
||||
isBedtimeMode={isBedtimeMode}
|
||||
onOpenPitchDeck={() => setIsPitchDeckOpen(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'activities' && (
|
||||
<ActivityCorner
|
||||
isBedtimeMode={isBedtimeMode}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className={`border-t py-8 px-4 sm:px-6 lg:px-8 transition-colors ${
|
||||
isBedtimeMode
|
||||
? 'bg-indigo-950/70 border-indigo-900 text-indigo-200'
|
||||
: 'bg-[#f7f2e7] border-amber-200/80 text-stone-700'
|
||||
}`}>
|
||||
<div className="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">🦔</span>
|
||||
<div>
|
||||
<p className="font-heading font-bold text-sm text-stone-900 dark:text-white">
|
||||
{BOOK_METADATA.title}
|
||||
</p>
|
||||
<p className="text-[11px] font-medium text-stone-600 dark:text-indigo-300">
|
||||
Children's Storybook Concept & Interactive Story Experience
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-4 text-xs font-semibold">
|
||||
<button
|
||||
onClick={() => { soundManager.playPop(); setActiveTab('reader'); }}
|
||||
className="hover:text-amber-600 transition-colors"
|
||||
>
|
||||
Story Reader
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { soundManager.playPop(); setActiveTab('storyboard'); }}
|
||||
className="hover:text-amber-600 transition-colors"
|
||||
>
|
||||
Storyboard
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { soundManager.playPop(); setActiveTab('art'); }}
|
||||
className="hover:text-amber-600 transition-colors"
|
||||
>
|
||||
Art Direction
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { soundManager.playPop(); setActiveTab('characters'); }}
|
||||
className="hover:text-amber-600 transition-colors"
|
||||
>
|
||||
Character Vault
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { soundManager.playPop(); setActiveTab('adaptability'); }}
|
||||
className="hover:text-amber-600 transition-colors"
|
||||
>
|
||||
Age Matrix
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { soundManager.playPop(); setActiveTab('publishing'); }}
|
||||
className="hover:text-amber-600 transition-colors"
|
||||
>
|
||||
Publishing Specs
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { soundManager.playPop(); setIsPitchDeckOpen(true); }}
|
||||
className="text-amber-700 dark:text-amber-400 font-bold hover:underline"
|
||||
>
|
||||
Editorial Pitch Deck
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 text-[11px] font-semibold text-stone-700 dark:text-indigo-200">
|
||||
<span>Crafted with</span>
|
||||
<Heart className="w-3.5 h-3.5 text-rose-500 fill-rose-500" />
|
||||
<span>by</span>
|
||||
<span className="font-heading font-bold text-stone-900 dark:text-white tracking-wide">aimirah</span>
|
||||
<span className="text-stone-500 dark:text-indigo-400">|</span>
|
||||
<span className="font-heading font-bold text-amber-800 dark:text-amber-300 tracking-wide">mi4inc</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* Editorial Pitch Deck Modal */}
|
||||
<PitchDeckModal
|
||||
isOpen={isPitchDeckOpen}
|
||||
onClose={() => setIsPitchDeckOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,250 @@
|
||||
// Web Audio API Synthesizer for Children's Storybook
|
||||
|
||||
class SoundEffectsManager {
|
||||
private ctx: AudioContext | null = null;
|
||||
public enabled: boolean = true;
|
||||
|
||||
private initCtx() {
|
||||
if (!this.ctx && typeof window !== 'undefined') {
|
||||
const AudioCtx = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
|
||||
if (AudioCtx) {
|
||||
this.ctx = new AudioCtx();
|
||||
}
|
||||
}
|
||||
if (this.ctx && this.ctx.state === 'suspended') {
|
||||
this.ctx.resume();
|
||||
}
|
||||
}
|
||||
|
||||
// Soft cheerful pop
|
||||
playPop() {
|
||||
if (!this.enabled) return;
|
||||
this.initCtx();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
|
||||
osc.type = 'sine';
|
||||
const now = this.ctx.currentTime;
|
||||
osc.frequency.setValueAtTime(400, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(800, now + 0.08);
|
||||
|
||||
gain.gain.setValueAtTime(0.18, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.09);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.1);
|
||||
}
|
||||
|
||||
// Pentatonic magical chime
|
||||
playChime() {
|
||||
if (!this.enabled) return;
|
||||
this.initCtx();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const notes = [523.25, 659.25, 783.99, 1046.5]; // C5, E5, G5, C6
|
||||
notes.forEach((freq, idx) => {
|
||||
if (!this.ctx) return;
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
|
||||
osc.type = 'triangle';
|
||||
const now = this.ctx.currentTime + idx * 0.07;
|
||||
osc.frequency.setValueAtTime(freq, now);
|
||||
|
||||
gain.gain.setValueAtTime(0.12, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.4);
|
||||
});
|
||||
}
|
||||
|
||||
// Soft gentle raindrop plop
|
||||
playRain() {
|
||||
if (!this.enabled) return;
|
||||
this.initCtx();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const drops = [700, 1100, 850];
|
||||
drops.forEach((freq, i) => {
|
||||
if (!this.ctx) return;
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
|
||||
const now = this.ctx.currentTime + i * 0.08;
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(freq, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(freq * 0.5, now + 0.07);
|
||||
|
||||
gain.gain.setValueAtTime(0.14, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.15);
|
||||
});
|
||||
}
|
||||
|
||||
// Harp strum
|
||||
playHarp() {
|
||||
if (!this.enabled) return;
|
||||
this.initCtx();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const notes = [392.00, 523.25, 659.25, 783.99, 1046.5]; // G4, C5, E5, G5, C6
|
||||
notes.forEach((freq, i) => {
|
||||
if (!this.ctx) return;
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
|
||||
const now = this.ctx.currentTime + i * 0.05;
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(freq, now);
|
||||
|
||||
gain.gain.setValueAtTime(0.1, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.6);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.65);
|
||||
});
|
||||
}
|
||||
|
||||
// Flutter / whoosh
|
||||
playFlutter() {
|
||||
if (!this.enabled) return;
|
||||
this.initCtx();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
|
||||
const now = this.ctx.currentTime;
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(220, now);
|
||||
osc.frequency.linearRampToValueAtTime(440, now + 0.1);
|
||||
osc.frequency.linearRampToValueAtTime(260, now + 0.2);
|
||||
|
||||
gain.gain.setValueAtTime(0.08, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.22);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.25);
|
||||
}
|
||||
|
||||
// Playful giggle / chirp
|
||||
playGiggle() {
|
||||
if (!this.enabled) return;
|
||||
this.initCtx();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const notes = [587.33, 739.99, 587.33, 880.00];
|
||||
notes.forEach((freq, i) => {
|
||||
if (!this.ctx) return;
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
|
||||
const now = this.ctx.currentTime + i * 0.06;
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(freq, now);
|
||||
|
||||
gain.gain.setValueAtTime(0.09, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.12);
|
||||
});
|
||||
}
|
||||
|
||||
// Page turn sound
|
||||
playPageTurn() {
|
||||
if (!this.enabled) return;
|
||||
this.initCtx();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const bufferSize = this.ctx.sampleRate * 0.15;
|
||||
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.3));
|
||||
}
|
||||
|
||||
const noise = this.ctx.createBufferSource();
|
||||
noise.buffer = buffer;
|
||||
|
||||
const filter = this.ctx.createBiquadFilter();
|
||||
filter.type = 'lowpass';
|
||||
filter.frequency.value = 1200;
|
||||
|
||||
const gain = this.ctx.createGain();
|
||||
const now = this.ctx.currentTime;
|
||||
gain.gain.setValueAtTime(0.12, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.14);
|
||||
|
||||
noise.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
noise.start(now);
|
||||
}
|
||||
|
||||
// Star Celebration fanfare
|
||||
playStarCelebration() {
|
||||
if (!this.enabled) return;
|
||||
this.initCtx();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const notes = [523.25, 659.25, 783.99, 1046.50, 1318.51];
|
||||
notes.forEach((freq, i) => {
|
||||
if (!this.ctx) return;
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
|
||||
const now = this.ctx.currentTime + i * 0.08;
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(freq, now);
|
||||
|
||||
gain.gain.setValueAtTime(0.15, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.45);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.5);
|
||||
});
|
||||
}
|
||||
|
||||
playSound(type: 'pop' | 'chime' | 'rain' | 'harp' | 'flutter' | 'giggle') {
|
||||
switch (type) {
|
||||
case 'pop': this.playPop(); break;
|
||||
case 'chime': this.playChime(); break;
|
||||
case 'rain': this.playRain(); break;
|
||||
case 'harp': this.playHarp(); break;
|
||||
case 'flutter': this.playFlutter(); break;
|
||||
case 'giggle': this.playGiggle(); break;
|
||||
default: this.playPop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const soundManager = new SoundEffectsManager();
|
||||
@@ -0,0 +1,80 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Class-based dark variant so Bedtime Mode toggle drives every dark: style */
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background-color: #fbf8f2;
|
||||
color: #2c2523;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
h1, h2, h3, .font-heading {
|
||||
font-family: 'Fredoka', 'Quicksand', sans-serif;
|
||||
}
|
||||
|
||||
.font-hand {
|
||||
font-family: 'Patrick Hand', cursive;
|
||||
}
|
||||
|
||||
.font-reading {
|
||||
font-family: 'Quicksand', 'Nunito', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Custom whimsical storybook effects */
|
||||
.book-shadow {
|
||||
box-shadow: 0 20px 40px -15px rgba(45, 25, 20, 0.22),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.05),
|
||||
0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.book-spine {
|
||||
box-shadow: inset 18px 0 30px -10px rgba(0, 0, 0, 0.15),
|
||||
inset -4px 0 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.paper-texture {
|
||||
background-color: #fdfaf5;
|
||||
background-image: radial-gradient(#ece4d4 0.75px, transparent 0.75px), radial-gradient(#ece4d4 0.75px, #fdfaf5 0.75px);
|
||||
background-size: 30px 30px;
|
||||
background-position: 0 0, 15px 15px;
|
||||
}
|
||||
|
||||
/* Dark paper texture for Bedtime Mode */
|
||||
.dark .paper-texture {
|
||||
background-color: #0f172a;
|
||||
background-image: radial-gradient(rgba(99, 102, 241, 0.25) 0.75px, transparent 0.75px), radial-gradient(rgba(99, 102, 241, 0.18) 0.75px, #0f172a 0.75px);
|
||||
}
|
||||
|
||||
.animate-float-slow {
|
||||
animation: floatSlow 5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-pulse-gentle {
|
||||
animation: gentlePulse 3.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-sparkle {
|
||||
animation: shimmerSparkle 3s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes floatSlow {
|
||||
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
||||
50% { transform: translateY(-8px) rotate(1.5deg); }
|
||||
}
|
||||
|
||||
@keyframes gentlePulse {
|
||||
0%, 100% { opacity: 0.9; transform: scale(1); }
|
||||
50% { opacity: 1; transform: scale(1.03); }
|
||||
}
|
||||
|
||||
@keyframes shimmerSparkle {
|
||||
0% { transform: scale(0.8) rotate(0deg); opacity: 0.4; }
|
||||
50% { transform: scale(1.15) rotate(90deg); opacity: 1; }
|
||||
100% { transform: scale(0.8) rotate(180deg); opacity: 0.4; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,98 @@
|
||||
export class StorySpeechNarrator {
|
||||
private synth: SpeechSynthesis | null = null;
|
||||
private currentUtterance: SpeechSynthesisUtterance | null = null;
|
||||
private onEndCallback: (() => void) | null = null;
|
||||
private onWordCallback: ((wordIndex: number) => void) | null = null;
|
||||
|
||||
constructor() {
|
||||
if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
|
||||
this.synth = window.speechSynthesis;
|
||||
}
|
||||
}
|
||||
|
||||
public isAvailable(): boolean {
|
||||
return this.synth !== null;
|
||||
}
|
||||
|
||||
public speak(
|
||||
text: string,
|
||||
options?: {
|
||||
rate?: number;
|
||||
pitch?: number;
|
||||
onEnd?: () => void;
|
||||
onWord?: (index: number) => void;
|
||||
}
|
||||
) {
|
||||
if (!this.synth) return;
|
||||
this.stop();
|
||||
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
utterance.rate = options?.rate ?? 0.9; // Slightly slower for children's story cadence
|
||||
utterance.pitch = options?.pitch ?? 1.05; // Slightly warmer/friendly pitch
|
||||
|
||||
// Find warm friendly voice
|
||||
const voices = this.synth.getVoices();
|
||||
const naturalVoice = voices.find(v =>
|
||||
v.lang.startsWith('en') &&
|
||||
(v.name.includes('Natural') || v.name.includes('Samantha') || v.name.includes('Karen') || v.name.includes('Daniel') || v.name.includes('Google'))
|
||||
) || voices.find(v => v.lang.startsWith('en'));
|
||||
|
||||
if (naturalVoice) {
|
||||
utterance.voice = naturalVoice;
|
||||
}
|
||||
|
||||
this.onEndCallback = options?.onEnd || null;
|
||||
this.onWordCallback = options?.onWord || null;
|
||||
|
||||
utterance.onend = () => {
|
||||
this.currentUtterance = null;
|
||||
if (this.onEndCallback) this.onEndCallback();
|
||||
};
|
||||
|
||||
utterance.onerror = () => {
|
||||
this.currentUtterance = null;
|
||||
if (this.onEndCallback) this.onEndCallback();
|
||||
};
|
||||
|
||||
let wordCount = 0;
|
||||
utterance.onboundary = (e) => {
|
||||
if (e.name === 'word') {
|
||||
if (this.onWordCallback) {
|
||||
this.onWordCallback(wordCount);
|
||||
}
|
||||
wordCount++;
|
||||
}
|
||||
};
|
||||
|
||||
this.currentUtterance = utterance;
|
||||
this.synth.speak(utterance);
|
||||
}
|
||||
|
||||
public stop() {
|
||||
if (!this.synth) return;
|
||||
this.synth.cancel();
|
||||
this.currentUtterance = null;
|
||||
}
|
||||
|
||||
public pause() {
|
||||
if (this.synth) {
|
||||
this.synth.pause();
|
||||
}
|
||||
}
|
||||
|
||||
public resume() {
|
||||
if (this.synth) {
|
||||
this.synth.resume();
|
||||
}
|
||||
}
|
||||
|
||||
public isSpeaking(): boolean {
|
||||
return this.synth ? this.synth.speaking : false;
|
||||
}
|
||||
|
||||
public getCurrentUtterance(): SpeechSynthesisUtterance | null {
|
||||
return this.currentUtterance;
|
||||
}
|
||||
}
|
||||
|
||||
export const speechNarrator = new StorySpeechNarrator();
|
||||
Reference in New Issue
Block a user