mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-28 19:05:31 +02:00
support for managed tts
This commit is contained in:
parent
779ad51f9f
commit
c845a7c40d
4 changed files with 84 additions and 61 deletions
|
|
@ -570,9 +570,13 @@ function App() {
|
||||||
|
|
||||||
// Check if voice is available on mount
|
// Check if voice is available on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.ipc.invoke('voice:getConfig', null).then(config => {
|
Promise.all([
|
||||||
|
window.ipc.invoke('voice:getConfig', null),
|
||||||
|
window.ipc.invoke('oauth:getState', null),
|
||||||
|
]).then(([config, oauthState]) => {
|
||||||
|
const rowboatConnected = oauthState.config?.rowboat?.connected ?? false
|
||||||
setVoiceAvailable(!!config.deepgram)
|
setVoiceAvailable(!!config.deepgram)
|
||||||
setTtsAvailable(!!config.elevenlabs)
|
setTtsAvailable(!!config.elevenlabs || rowboatConnected)
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
setVoiceAvailable(false)
|
setVoiceAvailable(false)
|
||||||
setTtsAvailable(false)
|
setTtsAvailable(false)
|
||||||
|
|
|
||||||
46
apps/x/packages/core/src/auth/tokens.ts
Normal file
46
apps/x/packages/core/src/auth/tokens.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import container from '../di/container.js';
|
||||||
|
import { IOAuthRepo } from './repo.js';
|
||||||
|
import { IClientRegistrationRepo } from './client-repo.js';
|
||||||
|
import { getProviderConfig } from './providers.js';
|
||||||
|
import * as oauthClient from './oauth-client.js';
|
||||||
|
|
||||||
|
export async function getAccessToken(): Promise<string> {
|
||||||
|
const oauthRepo = container.resolve<IOAuthRepo>('oauthRepo');
|
||||||
|
const { tokens } = await oauthRepo.read('rowboat');
|
||||||
|
if (!tokens) {
|
||||||
|
throw new Error('Not signed into Rowboat');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oauthClient.isTokenExpired(tokens)) {
|
||||||
|
return tokens.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokens.refresh_token) {
|
||||||
|
throw new Error('Rowboat token expired and no refresh token available. Please sign in again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerConfig = getProviderConfig('rowboat');
|
||||||
|
if (providerConfig.discovery.mode !== 'issuer') {
|
||||||
|
throw new Error('Rowboat provider requires issuer discovery mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientRepo = container.resolve<IClientRegistrationRepo>('clientRegistrationRepo');
|
||||||
|
const registration = await clientRepo.getClientRegistration('rowboat');
|
||||||
|
if (!registration) {
|
||||||
|
throw new Error('Rowboat client not registered. Please sign in again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await oauthClient.discoverConfiguration(
|
||||||
|
providerConfig.discovery.issuer,
|
||||||
|
registration.client_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshed = await oauthClient.refreshTokens(
|
||||||
|
config,
|
||||||
|
tokens.refresh_token,
|
||||||
|
tokens.scopes,
|
||||||
|
);
|
||||||
|
await oauthRepo.upsert('rowboat', { tokens: refreshed });
|
||||||
|
|
||||||
|
return refreshed.access_token;
|
||||||
|
}
|
||||||
|
|
@ -1,53 +1,8 @@
|
||||||
import { ProviderV2 } from '@ai-sdk/provider';
|
import { ProviderV2 } from '@ai-sdk/provider';
|
||||||
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
||||||
import container from '../di/container.js';
|
import { getAccessToken } from '../auth/tokens.js';
|
||||||
import { IOAuthRepo } from '../auth/repo.js';
|
|
||||||
import { IClientRegistrationRepo } from '../auth/client-repo.js';
|
|
||||||
import { getProviderConfig } from '../auth/providers.js';
|
|
||||||
import * as oauthClient from '../auth/oauth-client.js';
|
|
||||||
import { API_URL } from '../config/env.js';
|
import { API_URL } from '../config/env.js';
|
||||||
|
|
||||||
async function getAccessToken(): Promise<string> {
|
|
||||||
const oauthRepo = container.resolve<IOAuthRepo>('oauthRepo');
|
|
||||||
const { tokens } = await oauthRepo.read('rowboat');
|
|
||||||
if (!tokens) {
|
|
||||||
throw new Error('Not signed into Rowboat');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!oauthClient.isTokenExpired(tokens)) {
|
|
||||||
return tokens.access_token;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tokens.refresh_token) {
|
|
||||||
throw new Error('Rowboat token expired and no refresh token available. Please sign in again.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerConfig = getProviderConfig('rowboat');
|
|
||||||
if (providerConfig.discovery.mode !== 'issuer') {
|
|
||||||
throw new Error('Rowboat provider requires issuer discovery mode');
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientRepo = container.resolve<IClientRegistrationRepo>('clientRegistrationRepo');
|
|
||||||
const registration = await clientRepo.getClientRegistration('rowboat');
|
|
||||||
if (!registration) {
|
|
||||||
throw new Error('Rowboat client not registered. Please sign in again.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = await oauthClient.discoverConfiguration(
|
|
||||||
providerConfig.discovery.issuer,
|
|
||||||
registration.client_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const refreshed = await oauthClient.refreshTokens(
|
|
||||||
config,
|
|
||||||
tokens.refresh_token,
|
|
||||||
tokens.scopes,
|
|
||||||
);
|
|
||||||
await oauthRepo.upsert('rowboat', { tokens: refreshed });
|
|
||||||
|
|
||||||
return refreshed.access_token;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getGatewayProvider(): Promise<ProviderV2> {
|
export async function getGatewayProvider(): Promise<ProviderV2> {
|
||||||
const accessToken = await getAccessToken();
|
const accessToken = await getAccessToken();
|
||||||
return createOpenRouter({
|
return createOpenRouter({
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { isSignedIn } from '../account/account.js';
|
||||||
|
import { getAccessToken } from '../auth/tokens.js';
|
||||||
|
import { API_URL } from '../config/env.js';
|
||||||
|
|
||||||
const homedir = process.env.HOME || process.env.USERPROFILE || '';
|
const homedir = process.env.HOME || process.env.USERPROFILE || '';
|
||||||
|
|
||||||
|
|
@ -32,21 +35,36 @@ export async function getVoiceConfig(): Promise<VoiceConfig> {
|
||||||
|
|
||||||
export async function synthesizeSpeech(text: string): Promise<{ audioBase64: string; mimeType: string }> {
|
export async function synthesizeSpeech(text: string): Promise<{ audioBase64: string; mimeType: string }> {
|
||||||
const config = await getVoiceConfig();
|
const config = await getVoiceConfig();
|
||||||
if (!config.elevenlabs) {
|
const signedIn = await isSignedIn();
|
||||||
throw new Error('ElevenLabs not configured. Create ~/.rowboat/config/elevenlabs.json with { "apiKey": "<your-key>" }');
|
|
||||||
|
let url: string;
|
||||||
|
let headers: Record<string, string>;
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
|
const voiceId = config.elevenlabs?.voiceId || 'UgBBYS2sOqTuMpoF3BR0';
|
||||||
|
const accessToken = await getAccessToken();
|
||||||
|
url = `${API_URL}/v1/voice/text-to-speech/${voiceId}`;
|
||||||
|
headers = {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
console.log('[voice] synthesizing speech via Rowboat proxy, text length:', text.length, 'voiceId:', voiceId);
|
||||||
|
} else {
|
||||||
|
if (!config.elevenlabs) {
|
||||||
|
throw new Error('ElevenLabs not configured. Create ~/.rowboat/config/elevenlabs.json with { "apiKey": "<your-key>" }');
|
||||||
|
}
|
||||||
|
const voiceId = config.elevenlabs.voiceId || 'UgBBYS2sOqTuMpoF3BR0';
|
||||||
|
url = `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`;
|
||||||
|
headers = {
|
||||||
|
'xi-api-key': config.elevenlabs.apiKey,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
console.log('[voice] synthesizing speech via ElevenLabs, text length:', text.length, 'voiceId:', voiceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const voiceId = config.elevenlabs.voiceId || 'UgBBYS2sOqTuMpoF3BR0';
|
|
||||||
const url = `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`;
|
|
||||||
|
|
||||||
console.log('[voice] synthesizing speech, text length:', text.length, 'voiceId:', voiceId);
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers,
|
||||||
'xi-api-key': config.elevenlabs.apiKey,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
text,
|
text,
|
||||||
model_id: 'eleven_flash_v2_5',
|
model_id: 'eleven_flash_v2_5',
|
||||||
|
|
@ -59,8 +77,8 @@ export async function synthesizeSpeech(text: string): Promise<{ audioBase64: str
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errText = await response.text().catch(() => 'Unknown error');
|
const errText = await response.text().catch(() => 'Unknown error');
|
||||||
console.error('[voice] ElevenLabs API error:', response.status, errText);
|
console.error('[voice] TTS API error:', response.status, errText);
|
||||||
throw new Error(`ElevenLabs API error ${response.status}: ${errText}`);
|
throw new Error(`TTS API error ${response.status}: ${errText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue