diff --git a/capabilities/demo-speech-synthesis/.env.development b/capabilities/demo-speech-synthesis/.env.development new file mode 100644 index 0000000..b0b990f --- /dev/null +++ b/capabilities/demo-speech-synthesis/.env.development @@ -0,0 +1 @@ +VITE_CUTOS_BROKER_URL=mock diff --git a/capabilities/demo-speech-synthesis/.env.production b/capabilities/demo-speech-synthesis/.env.production new file mode 100644 index 0000000..ef96a64 --- /dev/null +++ b/capabilities/demo-speech-synthesis/.env.production @@ -0,0 +1 @@ +VITE_CUTOS_BROKER_URL=localhost diff --git a/capabilities/demo-speech-synthesis/.gitignore b/capabilities/demo-speech-synthesis/.gitignore new file mode 100644 index 0000000..2f98841 --- /dev/null +++ b/capabilities/demo-speech-synthesis/.gitignore @@ -0,0 +1,7 @@ +node_modules +dist +dist-ssr +release +*.local +*.log +.DS_Store diff --git a/capabilities/demo-speech-synthesis/README.md b/capabilities/demo-speech-synthesis/README.md new file mode 100644 index 0000000..b65e6e7 --- /dev/null +++ b/capabilities/demo-speech-synthesis/README.md @@ -0,0 +1,38 @@ +# Demo Speech Synthesis + +CUTOS 4.0 LWA demo for `@cutos/speech-synthesis`. It uses the browser-side Web Speech API and Windows-installed voices. It does not require a CUTOS Provider, Device Driver, or Gateway service. + +## Local Development + +Build the sibling SDK first: + +```sh +cd ../sdk +npm install +npm run build + +cd ../demo-speech-synthesis +npm install +npm run dev +``` + +## LWA Build And Release + +```sh +npx cutos lwa build +npx cutos login --username office +npx cutos lwa upload +npx cutos lwa list demo-speech-synthesis +npx cutos lwa publish --device +``` + +The release version is managed only by `public/config.json`. + +## Offline Validation + +1. Run the LWA on a Windows CUTOS Runtime. +2. Confirm **Speech API Ready** and at least one listed voice. +3. Prefer a voice marked **Local** for offline operation. +4. Enter text and click **Speak**; the Activity card should show start and completion events. + +If no voices are listed, install a Windows speech voice pack and restart the Runtime. Some WebView implementations require speech to start from a user interaction, so this demo always uses an explicit Speak button. diff --git a/capabilities/demo-speech-synthesis/index.html b/capabilities/demo-speech-synthesis/index.html new file mode 100644 index 0000000..9929de6 --- /dev/null +++ b/capabilities/demo-speech-synthesis/index.html @@ -0,0 +1,13 @@ + + + + + + + CUTOS Speech Synthesis + + +
+ + + diff --git a/capabilities/demo-speech-synthesis/package.json b/capabilities/demo-speech-synthesis/package.json new file mode 100644 index 0000000..290a90a --- /dev/null +++ b/capabilities/demo-speech-synthesis/package.json @@ -0,0 +1,29 @@ +{ + "name": "demo-speech-synthesis", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "cutos lwa build", + "build:app": "vue-tsc --noEmit && vite build", + "package": "cutos lwa package", + "validate": "cutos lwa validate", + "preview": "vite preview" + }, + "dependencies": { + "@cutos/core": "^4.0.8", + "@cutos/speech-synthesis": "^4.0.0", + "vue": "^3.4.31" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.5", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.40", + "postcss-import": "^16.1.0", + "tailwindcss": "^3.4.7", + "typescript": "^5.5.4", + "vite": "^5.3.4", + "vue-tsc": "^2.0.26" + } +} diff --git a/capabilities/demo-speech-synthesis/postcss.config.js b/capabilities/demo-speech-synthesis/postcss.config.js new file mode 100644 index 0000000..9d55d43 --- /dev/null +++ b/capabilities/demo-speech-synthesis/postcss.config.js @@ -0,0 +1,7 @@ +export default { + plugins: { + 'postcss-import': {}, + tailwindcss: {}, + autoprefixer: {} + } +} diff --git a/capabilities/demo-speech-synthesis/public/config.json b/capabilities/demo-speech-synthesis/public/config.json new file mode 100644 index 0000000..9522a7c --- /dev/null +++ b/capabilities/demo-speech-synthesis/public/config.json @@ -0,0 +1,11 @@ +{ + "name": "demo-speech-synthesis", + "version": "0.1.1", + "description": "CUTOS 4.0 offline Web Speech API text-to-speech demo.", + "params": { + "title": "CUTOS Speech Synthesis", + "subtitle": "Web Speech API · Windows Offline TTS", + "host": "localhost" + }, + "drvDependencies": {} +} diff --git a/capabilities/demo-speech-synthesis/public/thumbnail.png b/capabilities/demo-speech-synthesis/public/thumbnail.png new file mode 100644 index 0000000..83da7fa Binary files /dev/null and b/capabilities/demo-speech-synthesis/public/thumbnail.png differ diff --git a/capabilities/demo-speech-synthesis/src/App.vue b/capabilities/demo-speech-synthesis/src/App.vue new file mode 100644 index 0000000..02655af --- /dev/null +++ b/capabilities/demo-speech-synthesis/src/App.vue @@ -0,0 +1,205 @@ + + + diff --git a/capabilities/demo-speech-synthesis/src/assets/cutos.png b/capabilities/demo-speech-synthesis/src/assets/cutos.png new file mode 100644 index 0000000..9f73429 Binary files /dev/null and b/capabilities/demo-speech-synthesis/src/assets/cutos.png differ diff --git a/capabilities/demo-speech-synthesis/src/components/CutosTopbar.vue b/capabilities/demo-speech-synthesis/src/components/CutosTopbar.vue new file mode 100644 index 0000000..a1bf462 --- /dev/null +++ b/capabilities/demo-speech-synthesis/src/components/CutosTopbar.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/capabilities/demo-speech-synthesis/src/main.ts b/capabilities/demo-speech-synthesis/src/main.ts new file mode 100644 index 0000000..fe5bae3 --- /dev/null +++ b/capabilities/demo-speech-synthesis/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import App from './App.vue' +import './style.css' + +createApp(App).mount('#app') diff --git a/capabilities/demo-speech-synthesis/src/style.css b/capabilities/demo-speech-synthesis/src/style.css new file mode 100644 index 0000000..11f9307 --- /dev/null +++ b/capabilities/demo-speech-synthesis/src/style.css @@ -0,0 +1,9 @@ +:root { font-family: Inter, "Segoe UI", Arial, sans-serif; color: #20262c; background: #eef1f3; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; } +* { box-sizing: border-box; } body { margin: 0; min-width: 320px; min-height: 100vh; } button, input, select, textarea { font: inherit; }.app-shell { min-height: 100vh; background: #eef1f3; }.page { width: min(1420px, 100%); margin: 0 auto; padding: 28px clamp(18px, 3vw, 42px) 36px; } +.hero { display:flex; justify-content:space-between; align-items:flex-end; gap:24px; margin: 0 0 22px; }.eyebrow { margin:0 0 7px; color:#168c81; font-size:11px; font-weight:800; letter-spacing:.13em; }.hero h2,.card h3 { margin:0; }.hero h2 { font-size:28px; line-height:1.2; }.hero p:not(.eyebrow) { max-width:690px; margin:9px 0 0; color:#627079; line-height:1.55; }.sdk-badge { min-width:245px; display:flex; flex-direction:column; gap:4px; padding:12px 14px; border:1px solid #ced8dd; border-radius:8px; background:#fff; font-size:12px; }.sdk-badge span { color:#71808a; font-size:10px; font-weight:800; letter-spacing:.1em; }.sdk-badge strong { color:#26343b; } +.grid { display:grid; grid-template-columns:repeat(12,minmax(0,1fr)); gap:18px; }.card { min-width:0; padding:20px; border:1px solid #d6dfe3; border-radius:10px; background:#fff; box-shadow:0 1px 2px rgba(25,42,50,.04); }.compose-card { grid-column:span 7; }.voices-card { grid-column:span 5; }.controls-card { grid-column:span 4; }.activity-card { grid-column:span 8; }.card-heading { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:18px; }.card h3 { font-size:18px; }.state,.count { padding:5px 8px; border-radius:999px; background:#edf1f3; color:#65737b; font-size:11px; font-weight:700; }.state.active { background:#def5ee; color:#137867; }.state.paused { background:#fff1d8; color:#9a6413; } +.field-label { display:block; margin:0 0 7px; color:#53636b; font-size:12px; font-weight:700; }textarea,select { width:100%; border:1px solid #bbc9cf; border-radius:6px; background:#fff; color:#26343b; outline:none; }textarea { resize:vertical; padding:11px; line-height:1.55; }select { height:38px; padding:0 10px; }textarea:focus,select:focus { border-color:#16a394; box-shadow:0 0 0 3px rgba(22,163,148,.12); }.textarea-meta { display:flex; justify-content:space-between; gap:12px; margin:7px 0 16px; color:#839098; font-size:11px; }.action-row { display:flex; flex-wrap:wrap; gap:9px; }button { min-height:36px; padding:0 14px; border:1px solid #b9c6cc; border-radius:6px; background:#fff; color:#35444b; cursor:pointer; font-weight:700; font-size:13px; }button:hover:not(:disabled) { border-color:#71848d; }button:disabled { cursor:not-allowed; opacity:.45; }.primary { border-color:#168f81; background:#16a394; color:#fff; }.primary:hover:not(:disabled) { background:#118b7d; }.text-button { min-height:0; padding:0; border:0; color:#168c81; background:transparent; } +.filter-row { display:flex; align-items:flex-end; gap:16px; margin-bottom:16px; }.filter-row label:first-child { flex:1; }.filter-row label > span { display:block; margin-bottom:7px; color:#53636b; font-size:12px; font-weight:700; }.switch { display:flex; align-items:center; gap:7px; height:38px; color:#53636b; font-size:12px; white-space:nowrap; }.switch input { width:15px; height:15px; accent-color:#16a394; }.voice-note { margin:13px 0 0; color:#728087; font-size:12px; line-height:1.45; }.voice-note strong { color:#168c81; } +.range { display:block; margin:0 0 21px; }.range:last-child { margin-bottom:0; }.range span { display:flex; justify-content:space-between; margin-bottom:8px; color:#53636b; font-size:12px; font-weight:700; }.range b { color:#168c81; }.range input { width:100%; accent-color:#16a394; }.activity-list { max-height:197px; overflow:auto; border-top:1px solid #e7edef; }.activity-item { display:grid; grid-template-columns:75px 1fr; gap:10px; padding:9px 1px; border-bottom:1px solid #edf1f3; font-size:12px; line-height:1.4; }.activity-item time { color:#87949b; }.activity-item.success span { color:#157969; }.activity-item.warning span { color:#9a6413; }.activity-item.error span { color:#b34d49; }.empty { margin:17px 0; color:#87949b; font-size:12px; } +@media (max-width:900px) { .compose-card,.voices-card,.controls-card,.activity-card { grid-column:span 6; }.hero { align-items:flex-start; flex-direction:column; }.sdk-badge { min-width:0; width:100%; } } +@media (max-width:640px) { .page { padding:20px 14px 28px; }.grid { grid-template-columns:1fr; gap:14px; }.compose-card,.voices-card,.controls-card,.activity-card { grid-column:span 1; }.card { padding:17px; }.filter-row { align-items:flex-start; flex-direction:column; gap:10px; }.textarea-meta { align-items:flex-start; flex-direction:column; gap:3px; } } diff --git a/capabilities/demo-speech-synthesis/src/utils/config.ts b/capabilities/demo-speech-synthesis/src/utils/config.ts new file mode 100644 index 0000000..a77d233 --- /dev/null +++ b/capabilities/demo-speech-synthesis/src/utils/config.ts @@ -0,0 +1,34 @@ +export interface LwaConfig { + name: string + version: string + params: Record +} + +const fallback: LwaConfig = { + name: 'demo-speech-synthesis', + version: '0.1.1', + params: { title: 'CUTOS Speech Synthesis', subtitle: 'Web Speech API · Windows Offline TTS', host: 'localhost' } +} + +function parseParams() { + const raw = new URLSearchParams(location.search).get('params') + if (!raw) return {} + try { + const value = JSON.parse(raw) + return value && typeof value === 'object' ? value as Record : {} + } catch { + return {} + } +} + +export async function loadConfig(): Promise { + try { + const url = new URL('config.json', window.location.href).toString() + const response = await fetch(url, { cache: 'no-cache' }) + if (!response.ok) throw new Error(`Failed to load config.json: ${response.status}`) + const config = await response.json() as LwaConfig + return { ...fallback, ...config, params: { ...fallback.params, ...config.params, ...parseParams() } } + } catch { + return { ...fallback, params: { ...fallback.params, ...parseParams() } } + } +} diff --git a/capabilities/demo-speech-synthesis/src/vite-env.d.ts b/capabilities/demo-speech-synthesis/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/capabilities/demo-speech-synthesis/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/capabilities/demo-speech-synthesis/tailwind.config.js b/capabilities/demo-speech-synthesis/tailwind.config.js new file mode 100644 index 0000000..d008c38 --- /dev/null +++ b/capabilities/demo-speech-synthesis/tailwind.config.js @@ -0,0 +1,6 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{vue,ts}'], + theme: { extend: {} }, + plugins: [] +} diff --git a/capabilities/demo-speech-synthesis/tsconfig.json b/capabilities/demo-speech-synthesis/tsconfig.json new file mode 100644 index 0000000..9250956 --- /dev/null +++ b/capabilities/demo-speech-synthesis/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": false, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "baseUrl": ".", + "paths": { "@/*": ["src/*"] } + }, + "include": ["src/**/*.ts", "src/**/*.vue"] +} diff --git a/capabilities/demo-speech-synthesis/vite.config.ts b/capabilities/demo-speech-synthesis/vite.config.ts new file mode 100644 index 0000000..1444291 --- /dev/null +++ b/capabilities/demo-speech-synthesis/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + base: './', + publicDir: 'public', + build: { target: ['chrome74'] }, + resolve: { alias: { '@': '/src' } }, + plugins: [vue()], + optimizeDeps: { + esbuildOptions: { + define: { global: 'globalThis' }, + target: 'es2015', + supported: { bigint: true } + } + } +})