Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 0 additions & 3 deletions packages/chrome/.env.example

This file was deleted.

43 changes: 4 additions & 39 deletions packages/chrome/README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

5 changes: 3 additions & 2 deletions packages/chrome/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Binary file added packages/chrome/preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
67 changes: 31 additions & 36 deletions packages/chrome/public/manifest.json
Original file line number Diff line number Diff line change
@@ -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": ["<all_urls>"],
"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": ["<all_urls>"],
"js": ["overlay.js"],
"run_at": "document_end"
}
],
"web_accessible_resources": [
{ "resources": ["fonts/*"], "matches": ["https://*/*"] }
]
}
67 changes: 34 additions & 33 deletions packages/chrome/src/background/background.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,42 @@
import { fetchResponse, type TTSOptions } from "../utils/tts-provider";

async function blobToDataURL(blob: Blob): Promise<string> {
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"],
// });
// });
71 changes: 33 additions & 38 deletions packages/chrome/src/popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(
null,
);


useEffect(() => {
return () => {
if (audioElement) {
audioElement.pause();
}
};
}, [audioElement]);

return (
<main className="p-4 text-white bg-gray-100/50 border w-[343.08px]">
<Header />

<JapaneseInputForm
setError={setError}

setAudioDataUrl={setAudioDataUrl}
audioElement={audioElement}
/>

{error && <ErrorUI error={error || "エラーが発生しました"} />}

{audioDataUrl && (
<AudioPlayer
src={audioDataUrl}
autoPlay={true}
onAudioElement={setAudioElement}
/>
)}
</main>
);
const [audioDataUrl, setAudioDataUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(
null,
);

useEffect(() => {
return () => {
if (audioElement) {
audioElement.pause();
}
};
}, [audioElement]);

return (
<main className="p-4 text-white bg-gray-100/50 border w-[343.08px]">
<JapaneseInputForm
setError={setError}
setAudioDataUrl={setAudioDataUrl}
audioElement={audioElement}
/>

{error && <ErrorUI error={error || "エラーが発生しました"} />}

{audioDataUrl && (
<AudioPlayer
src={audioDataUrl}
autoPlay={true}
onAudioElement={setAudioElement}
/>
)}
</main>
);
};

export default Popup;
9 changes: 0 additions & 9 deletions packages/chrome/src/popup/components/header.tsx

This file was deleted.

Loading
Loading