Skip to content
Merged
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
.env
.vscode
node_modules
dist
dist
coverage
.DS_Store
tmp
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,24 @@ await client.stopPlayback();
await client.stopTranslation();
```

### 6. Output device changing

> [!NOTE]
> Audio output device switching is supported only in browsers that implement setSinkId(). In unsupported browsers like Safari, this method will have no effect.
>
```ts
await client.changeAudioOutputDevice('deviceId')
```

### 7. Volume changing for audio track by language

> [!NOTE]
> Volume should be a value between 0.0 and 1.0, where 0.0 is muted and 1.0 is maximum volume.
>
```ts
client.setVolume('es', .7)
```

> [!NOTE]
> Browsers may restrict audio playback initiated without user interaction.
> Each browser may also define user interaction differently.
Expand Down Expand Up @@ -145,6 +163,14 @@ new PalabraClient(options: PalabraClientData)
- `unmuteOriginalTrack(): void`
Unmutes the original audio track (microphone).

- `setVolume(language: string, volume: number): void`
Set volume for audio track by given language. Volume should be between 0.0 (muted) and 1.0 (maximum)

- `changeAudioOutputDevice(deviceId: string): Promise<void>`
Change output device
> Note: Audio output device switching is supported only in browsers that implement setSinkId(). In unsupported browsers like Safari, this method will have no effect.
>

- `cleanup(): Promise<void>`
Stops translation and playback, releases resources, and resets the client to its initial state.

Expand Down
1 change: 1 addition & 0 deletions packages/dev-app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ coverage

test-results/
playwright-report/
*.test.mp3
23 changes: 23 additions & 0 deletions packages/dev-app/src/components/VolumeChanger.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script setup lang="ts">
import type { PalabraClient } from '@palabra-ai/translator';
import { ref, watch } from 'vue';

const props = defineProps<{
client: PalabraClient;
language: string;
}>();

const volume = ref(1);

watch(volume, (newVolume) => {
props.client.setVolume(props.language, newVolume);
});
</script>

<template>
<div>
<input type="range" v-model="volume" min="0" max="1" step="0.01" />

{{ Math.round(volume * 100) }} % [{{ language }}]
</div>
</template>
12 changes: 12 additions & 0 deletions packages/dev-app/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import HomeView from '../views/HomeView.vue'
import BasicTranslator from '../views/examples/BasicTranslator.vue'
import AdvancedTranslator from '../views/examples/AdvancedTranslator.vue'
import AudioElement from '../views/examples/AudioElement.vue'
import FromAudioFile from '../views/examples/FromAudioFile.vue'
import ChangeAudioOutputDevice from '@/views/examples/ChangeAudioOutputDevice.vue'

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
Expand All @@ -26,6 +28,16 @@ const router = createRouter({
path: '/audio-element',
name: 'audio-element',
component: AudioElement
},
{
path: '/from-audio-file',
name: 'from-audio-file',
component: FromAudioFile
},
{
path: '/change-audio-output-device',
name: 'change-audio-output-device',
component: ChangeAudioOutputDevice
}
]
},
Expand Down
15 changes: 15 additions & 0 deletions packages/dev-app/src/views/HomeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const route = useRoute();
</div>
</div>
<nav class="flex items-center md:flex-row flex-col">

<router-link
to="/"
class="mx-4 px-4 py-2 rounded-md transition-colors"
Expand All @@ -38,6 +39,20 @@ const route = useRoute();
>
Advanced Example
</router-link>
<router-link
to="/from-audio-file"
class="mx-4 px-4 py-2 rounded-md transition-colors"
:class="route.path === '/from-audio-file' ? 'bg-blue-100 text-blue-600' : 'hover:bg-gray-100'"
>
From Audio File Example
</router-link>
<router-link
to="/change-audio-output-device"
class="mx-4 px-4 py-2 rounded-md transition-colors"
:class="route.path === '/change-audio-output-device' ? 'bg-blue-100 text-blue-600' : 'hover:bg-gray-100'"
>
Change Audio Output Device
</router-link>
</nav>
</header>
<main class="flex flex-col items-center justify-center h-[calc(100vh-4rem)] gap-4">
Expand Down
7 changes: 5 additions & 2 deletions packages/dev-app/src/views/examples/AdvancedTranslator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import {
type PalabraClientData,
} from '@palabra-ai/translator';
import { onMounted, ref } from 'vue';
import VolumeChanger from '@/components/VolumeChanger.vue';

let palabraClient: PalabraClient | null = null;
const currentMicTrack: MediaStreamTrack | null = null;

const isTranslationStarted = ref(false);
const isMicrophoneMuted = ref(false);
Expand Down Expand Up @@ -58,7 +58,6 @@ const stopTranslation = async () => {
};

const toggleMicrophone = () => {
if (!currentMicTrack) return;
if (isMicrophoneMuted.value) {
palabraClient?.unmuteOriginalTrack();
isMicrophoneMuted.value = false;
Expand Down Expand Up @@ -110,6 +109,10 @@ const setTranslateTo = async (code: PalabraClientData['translateTo']) => {
<template>
<main class="flex flex-col items-center justify-center h-[calc(100vh-4rem)] gap-4">
<h2 class="text-2xl font-bold mb-6 text-center">Advanced Translator Example</h2>
<div v-if="isTranslationStarted && palabraClient">
<VolumeChanger :client="palabraClient" language="es" />
<VolumeChanger :client="palabraClient" language="fr" />
</div>
<div>
<h2>Transcription:</h2>
<p>{{ transcriptionData }}</p>
Expand Down
195 changes: 195 additions & 0 deletions packages/dev-app/src/views/examples/ChangeAudioOutputDevice.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<script setup lang="ts">
import {
getLocalAudioTrack,
PalabraClient,
EVENT_TRANSLATION_RECEIVED,
EVENT_PARTIAL_TRANSCRIPTION_RECEIVED,
EVENT_REMOTE_TRACKS_UPDATE,
EVENT_DATA_RECEIVED,
getAudioOutputDevices,
PalabraApiClient,
} from '@palabra-ai/translator';
import { onMounted, ref, watch } from 'vue';
import VolumeChanger from '@/components/VolumeChanger.vue';

let palabraClient: PalabraClient | null = null;
const isTranslationStarted = ref(false);
const isMicrophoneMuted = ref(false);

const transcriptionData = ref<string>('');
const translationData = ref<string>('');
const audioOutputDevice = ref<string>('default');
const audioOutputDevices = ref<MediaDeviceInfo[]>([]);

const buttonClass = 'flex-1 text-white p-3 rounded-md disabled:opacity-50 disabled:cursor-not-allowed';
const clearSessions = async () => {
const apiClient = new PalabraApiClient({
clientId: import.meta.env.VITE_PALABRA_CLIENT_ID,
clientSecret: import.meta.env.VITE_PALABRA_CLIENT_SECRET,
}, import.meta.env.VITE_PALABRA_ENDPOINT);

const sessions = await apiClient.fetchActiveSessions();
sessions?.data?.sessions?.forEach(session => {
apiClient.deleteStreamingSession(session.id);
});
}

const handleClientEvents = () => {
if (!palabraClient) return;

palabraClient.on(EVENT_DATA_RECEIVED, (data) => {
console.log('Data received:', data);
});

palabraClient.on(EVENT_PARTIAL_TRANSCRIPTION_RECEIVED, (data) => {
if (data) {
transcriptionData.value = data.transcription.text;
}
});

palabraClient.on(EVENT_TRANSLATION_RECEIVED, (data) => {
if (data) {
translationData.value = data.transcription.text;
}
});

palabraClient.on(EVENT_REMOTE_TRACKS_UPDATE, (data) => {
console.log('Remote tracks updated:', data);
});

palabraClient.on(EVENT_DATA_RECEIVED, (data) => {
console.log('Data received EVENT_DATA_RECEIVED:', data);
});
}

watch(audioOutputDevice, async (newVal) => {
console.log('audioOutputDevice', newVal);
if (palabraClient) {
await palabraClient.changeAudioOutputDevice(newVal);
}
});

onMounted(async () => {
audioOutputDevices.value = await getAudioOutputDevices();

await clearSessions();

palabraClient = new PalabraClient({
auth: {
clientId: import.meta.env.VITE_PALABRA_CLIENT_ID,
clientSecret: import.meta.env.VITE_PALABRA_CLIENT_SECRET,
},
translateFrom: 'en',
translateTo: 'es',
handleOriginalTrack: getLocalAudioTrack,
apiBaseUrl: import.meta.env.VITE_PALABRA_ENDPOINT,
});

handleClientEvents();
});

const startTranslation = async () => {
try {
isTranslationStarted.value = true;
await palabraClient?.startTranslation();
await palabraClient?.startPlayback();
} catch (error) {
await stopTranslation();
alert(error);
}
};

const stopTranslation = async () => {
isTranslationStarted.value = false;
isMicrophoneMuted.value = false;
await palabraClient?.stopTranslation();
reset();
await palabraClient?.cleanup();
};

const toggleMicrophone = () => {
if (!palabraClient) return;

if (isMicrophoneMuted.value) {
palabraClient.unmuteOriginalTrack();
isMicrophoneMuted.value = false;
} else {
palabraClient.muteOriginalTrack();
isMicrophoneMuted.value = true;
}
};

const reset = () => {
transcriptionData.value = '';
translationData.value = '';
};
</script>

<template>
<div class="container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto flex flex-col gap-4">
<h2 class="text-2xl font-bold mb-6 text-center">Basic Translator Example</h2>



<div class="flex flex-col gap-4 shadow-md p-6 rounded-lg">
<h1>Change Audio Output Device</h1>
<select v-model="audioOutputDevice" class="w-full p-2 rounded-md border border-gray-300">
<option v-for="device in audioOutputDevices" :key="device.deviceId" :value="device.deviceId">
{{ device.label }}
</option>
</select>

<VolumeChanger v-if="isTranslationStarted && palabraClient" :client="palabraClient" language="es" />
</div>
<div class="bg-white rounded-lg shadow-lg p-6 flex flex-col gap-7">
<div>
<h3>Transcription:</h3>
<p class="text-sm text-gray-600">{{ transcriptionData }}</p>
</div>

<div class="flex items-center gap-4 md:flex-row flex-col">
<button
:disabled="isTranslationStarted"
:class="buttonClass"
class="bg-blue-500 hover:bg-blue-600"
@click="startTranslation"
>
Start Translation
</button>
<button
:disabled="!isTranslationStarted"
:class="buttonClass"
class="bg-red-500 hover:bg-red-600"
@click="stopTranslation"
>
Stop Translation
</button>
<button
:disabled="!isTranslationStarted"
:class="[
buttonClass,
isMicrophoneMuted ? 'bg-green-500 hover:bg-green-600' : 'bg-yellow-500 hover:bg-yellow-600'
]"
@click="toggleMicrophone"
>
{{ isMicrophoneMuted ? '🔇 Unmute microphone' : '🎤 Mute microphone' }}
</button>
</div>
<div class="mb-14">
<h3>Translation:</h3>
<p class="text-sm text-gray-600">{{ translationData }}</p>
</div>
<div class="text-sm text-gray-600">
<p>This is a basic example demonstrating:</p>
<ul class="list-disc ml-6 mt-2">
<li>Starting/stopping translation</li>
<li>Microphone muting</li>
<li>Basic error handling</li>
<li>Transcription and translation data</li>
</ul>
</div>
</div>
</div>
</div>
</template>
Loading