diff --git a/package.json b/package.json index d8053bc..2a2d291 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "hanashi", "version": "0.0.1", "description": "A Chrome extension for Japanese speech recognition", + "type": "module", "scripts": { "build": "pnpm --filter hanashi-chrome build", "test": "pnpm --filter hanashi-chrome test", diff --git a/packages/chrome/.env.example b/packages/chrome/.env.example deleted file mode 100644 index de5749a..0000000 --- a/packages/chrome/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -# ElevenLabs API Configuration -# Get your API key from: https://elevenlabs.io/app/settings/api-keys -VITE_ELEVENLABS_API_KEY=your_elevenlabs_api_key_here diff --git a/packages/chrome/README.md b/packages/chrome/README.md index 65182f4..cbe6459 100644 --- a/packages/chrome/README.md +++ b/packages/chrome/README.md @@ -1,38 +1,11 @@ -# Nihongo Speech +# Hanashi -A Chrome extension for converting Japanese text to realistic speech using ElevenLabs AI voices. +A Chrome extension for converting Japanese text to realistic speech. ## Features -![CleanShot 2025-05-28 at 16 59 01@2x](https://github.com/user-attachments/assets/52f7dc5a-5f9e-4549-8c02-3bf2c7a7fce1) - - -- 🎌 **Japanese TTS**: Convert Japanese text to natural-sounding speech -- 👥 **Multiple Voices**: Choose between male (Asahi) and female (Morioki) voices -- 🎵 **Audio Player**: Built-in player with progress and volume controls -- 🔒 **Secure**: API keys stored locally in Chrome storage -- 🎨 **Modern UI**: Clean interface with rounded corners and smooth animations - -## Setup - -### 1. Get ElevenLabs API Key - -1. Sign up at [ElevenLabs](https://elevenlabs.io/) -2. Go to [API Settings](https://elevenlabs.io/app/settings/api-keys) -3. Create a new API key - -### 2. Install Extension - -1. Clone this repository -2. Run `npm install` -3. Run `npm run build:prod` -4. Load the `dist` folder as an unpacked extension in Chrome - -### 3. Configure API Key - -1. Click the extension icon -2. Enter your ElevenLabs API key in the settings -3. Start converting Japanese text to speech! +- **Japanese TTS**: Convert Japanese text to natural-sounding speech +- **Voice Selection**: Choose between male and female voices ## Development @@ -49,11 +22,3 @@ npm run build:prod # Development server npm run dev ``` - -## Security - -- ✅ **No hardcoded API keys** in source code -- ✅ **Local storage** - API keys stored in Chrome's secure storage -- ✅ **Minimal permissions** - only accesses ElevenLabs API -- ✅ **No data collection** - all processing happens locally - diff --git a/packages/chrome/package.json b/packages/chrome/package.json index 1a44c25..091d687 100644 --- a/packages/chrome/package.json +++ b/packages/chrome/package.json @@ -3,13 +3,14 @@ "version": "1.2.0", "description": "Chrome extension for Japanese TTS with realistic voices.", "scripts": { - "dev": "vite", + "dev": "VITE_CJS_TRACE=true vite", "build": "vite build --mode development", "build:watch": "vite build --mode development --watch", "build:prod": "vite build --mode production", "preview": "vite preview", "test": "vitest", - "test:ui": "vitest --ui" + "test:ui": "vitest --ui", + "package": "npm run build:prod && cd dist && zip -r ../nihongo-speech-extension.zip ." }, "dependencies": { "chrome-types": "^0.1.357", diff --git a/packages/chrome/preview.png b/packages/chrome/preview.png new file mode 100644 index 0000000..0290e1c Binary files /dev/null and b/packages/chrome/preview.png differ diff --git a/packages/chrome/public/manifest.json b/packages/chrome/public/manifest.json index 780ce5f..9b255fb 100644 --- a/packages/chrome/public/manifest.json +++ b/packages/chrome/public/manifest.json @@ -1,38 +1,33 @@ { - "manifest_version": 3, - "name": "Nihongo Speech", - "version": "1.1.0", - "description": "Convert Japanese sentences to realistic male and female speech using advanced TTS APIs.", - "icons": { - "16": "icon-16.png", - "48": "icon-48.png", - "128": "icon-128.png" - }, - "action": { - "default_popup": "popup.html", - "default_icon": { - "16": "icon-16.png", - "48": "icon-48.png", - "128": "icon-128.png" - } - }, - "background": { - "service_worker": "background.js" - }, - "permissions": ["storage", "contextMenus", "activeTab", "scripting"], - "host_permissions": [ - "https://api.elevenlabs.io/*", - "http://localhost:*/*", - "http://127.0.0.1:*/*" - ], - "content_scripts": [ - { - "matches": [""], - "js": ["overlay.js"], - "run_at": "document_end" - } - ], - "web_accessible_resources": [ - { "resources": ["fonts/*"], "matches": ["https://*/*"] } - ] + "manifest_version": 3, + "name": "Hanashi", + "version": "1.2.0", + "description": "Convert Japanese text to realistic speech.", + "icons": { + "16": "icon-16.png", + "48": "icon-48.png", + "128": "icon-128.png" + }, + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icon-16.png", + "48": "icon-48.png", + "128": "icon-128.png" + } + }, + "background": { + "service_worker": "background.js" + }, + "host_permissions": ["https://hanshi-server.adelekekehinde06.workers.dev"], + "content_scripts": [ + { + "matches": [""], + "js": ["overlay.js"], + "run_at": "document_end" + } + ], + "web_accessible_resources": [ + { "resources": ["fonts/*"], "matches": ["https://*/*"] } + ] } diff --git a/packages/chrome/src/background/background.ts b/packages/chrome/src/background/background.ts index bf8db93..064a6a4 100644 --- a/packages/chrome/src/background/background.ts +++ b/packages/chrome/src/background/background.ts @@ -1,41 +1,42 @@ import { fetchResponse, type TTSOptions } from "../utils/tts-provider"; async function blobToDataURL(blob: Blob): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => { - if (typeof reader.result === "string") { - resolve(reader.result); - } else { - reject(new Error("Failed to convert blob to data URL.")); - } - }; - reader.onerror = reject; - reader.readAsDataURL(blob); - }); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + if (typeof reader.result === "string") { + resolve(reader.result); + } else { + reject(new Error("Failed to convert blob to data URL.")); + } + }; + reader.onerror = reject; + reader.readAsDataURL(blob); + }); } chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { - if (message.type === "TTS_REQUEST") { - const options: TTSOptions = message.payload; - fetchResponse(options) - .then((blob) => blobToDataURL(blob)) - .then((dataUrl) => { - sendResponse({ success: true, dataUrl }); - }) - .catch((error) => { - console.error("Error in background script TTS request:", error); - sendResponse({ success: false, error: error.message }); - }); - return true; - } + if (message.type === "TTS_REQUEST") { + const options: TTSOptions = message.payload; + fetchResponse(options) + .then((blob) => blobToDataURL(blob)) + .then((dataUrl) => { + sendResponse({ success: true, dataUrl }); + }) + .catch((error) => { + console.error("Error in background script TTS request:", error); + sendResponse({ success: false, error: error.message }); + }); + return true; + } }); -const ID = "nihongo-speech"; -chrome.runtime.onInstalled.addListener(() => { - chrome.contextMenus.create({ - id: ID, - title: "Generate Speech in nihongo-speech", - contexts: ["selection"], - }); -}); +// TODO: Add context menu support +// const ID = "hanashi"; +// chrome.runtime.onInstalled.addListener(() => { +// chrome.contextMenus.create({ +// id: ID, +// title: "Generate Speech in hanashi", +// contexts: ["selection"], +// }); +// }); diff --git a/packages/chrome/src/popup/Popup.tsx b/packages/chrome/src/popup/Popup.tsx index a2353f5..01adad6 100644 --- a/packages/chrome/src/popup/Popup.tsx +++ b/packages/chrome/src/popup/Popup.tsx @@ -2,47 +2,42 @@ import { useEffect, useState } from "react"; import "./global.css"; import AudioPlayer from "./audio-player"; import { ErrorUI } from "./components/error"; -import { Header } from "./components/header"; import { JapaneseInputForm } from "./components/japanese-input-form"; const Popup = () => { - const [audioDataUrl, setAudioDataUrl] = useState(null); - const [error, setError] = useState(null); - const [audioElement, setAudioElement] = useState( - null, - ); - - - useEffect(() => { - return () => { - if (audioElement) { - audioElement.pause(); - } - }; - }, [audioElement]); - - return ( -
-
- - - - {error && } - - {audioDataUrl && ( - - )} -
- ); + const [audioDataUrl, setAudioDataUrl] = useState(null); + const [error, setError] = useState(null); + const [audioElement, setAudioElement] = useState( + null, + ); + + useEffect(() => { + return () => { + if (audioElement) { + audioElement.pause(); + } + }; + }, [audioElement]); + + return ( +
+ + + {error && } + + {audioDataUrl && ( + + )} +
+ ); }; export default Popup; diff --git a/packages/chrome/src/popup/components/header.tsx b/packages/chrome/src/popup/components/header.tsx deleted file mode 100644 index 49b1587..0000000 --- a/packages/chrome/src/popup/components/header.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export const Header = () => { - return ( -
-

- Hanashi 🌸 -

-
- ); -}; diff --git a/packages/chrome/src/popup/components/japanese-input-form.tsx b/packages/chrome/src/popup/components/japanese-input-form.tsx index 596292e..d5667a1 100644 --- a/packages/chrome/src/popup/components/japanese-input-form.tsx +++ b/packages/chrome/src/popup/components/japanese-input-form.tsx @@ -1,132 +1,113 @@ import { useRef, useState } from "react"; export const JapaneseInputForm = ({ - setError, - setAudioDataUrl, - audioElement, + setError, + setAudioDataUrl, + audioElement, }: { - setError: (error: string | null) => void; - setAudioDataUrl: (audioDataUrl: string | null) => void; - audioElement: HTMLAudioElement | null; + setError: (error: string | null) => void; + setAudioDataUrl: (audioDataUrl: string | null) => void; + audioElement: HTMLAudioElement | null; }) => { - const [text, setText] = useState(""); - const [gender, setGender] = useState<"male" | "female">("male"); - const [loading, setLoading] = useState(false); - const formRef = useRef(null); - const [fontFamily, setFontFamily] = useState("font-stick"); + const [text, setText] = useState(""); + const [gender, setGender] = useState<"male" | "female">("male"); + const [loading, setLoading] = useState(false); + const formRef = useRef(null); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); - setLoading(true); - setError(null); - setAudioDataUrl(null); - chrome.storage.local.remove(["savedAudioUrl"]); - if (audioElement) audioElement.pause(); - if (!chrome || !chrome.runtime || !chrome.runtime.sendMessage) { - setLoading(false); - setError("Cannot send TTS request outside of extension context."); - console.warn( - "chrome.runtime.sendMessage not available. Running in dev mode?", - ); - return; - } + setLoading(true); + setError(null); + setAudioDataUrl(null); + if (audioElement) audioElement.pause(); + if (!chrome || !chrome.runtime || !chrome.runtime.sendMessage) { + setLoading(false); + setError("Cannot send TTS request outside of extension context."); + console.warn( + "chrome.runtime.sendMessage not available. Running in dev mode?", + ); + return; + } - chrome.runtime.sendMessage( - { - type: "TTS_REQUEST", - payload: { text, gender }, - }, - (response) => { - setLoading(false); - if (chrome.runtime.lastError) { - console.error( - "TTS Request failed:", - chrome.runtime.lastError.message, - ); - setError(`Error: ${chrome.runtime.lastError.message}`); - return; - } + chrome.runtime.sendMessage( + { + type: "TTS_REQUEST", + payload: { text, gender }, + }, + (response) => { + setLoading(false); + if (chrome.runtime.lastError) { + console.error( + "TTS Request failed:", + chrome.runtime.lastError.message, + ); + setError(`Error: ${chrome.runtime.lastError.message}`); + return; + } - if (!response?.success) { - console.error("TTS Response error:", response?.error); - setError(response?.error || "Unknown error from background script"); - return; - } + if (!response?.success) { + console.error("TTS Response error:", response?.error); + setError(response?.error || "Unknown error from background script"); + return; + } - setAudioDataUrl(response.dataUrl); - chrome.storage.local.set({ savedAudioUrl: response.dataUrl }); - }, - ); - }; - return ( -
-