mirror of
https://github.com/kunkunsh/kunkun-ext-speech-to-text.git
synced 2025-04-03 17:56:43 +00:00
Initial project setup for Kunkun Speech to Text extension
- Added project configuration files (tsconfig, eslint, vite, etc.) - Implemented Svelte5 frontend with routing - Created Deno backend for audio transcription using OpenAI Whisper - Added preferences page for API key configuration - Configured Tailwind CSS and theming - Implemented file selection and transcription functionality
This commit is contained in:
commit
51fcb8285e
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
extensions_support/
|
||||||
|
|
||||||
|
*.m4a
|
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Package Managers
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
15
.prettierrc
Normal file
15
.prettierrc
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.svelte",
|
||||||
|
"options": {
|
||||||
|
"parser": "svelte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
4
README.md
Normal file
4
README.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Kunkun Speech to Text Extension
|
||||||
|
|
||||||
|
This extension helps you to convert an audio file to text.
|
||||||
|
It uses OpenAI's Whisper model API to convert the audio to text.
|
14
components.json
Normal file
14
components.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/app.css",
|
||||||
|
"baseColor": "neutral"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils"
|
||||||
|
},
|
||||||
|
"typescript": true
|
||||||
|
}
|
11
deno-src/deno.json
Normal file
11
deno-src/deno.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"tasks": {
|
||||||
|
"dev": "deno run --watch main.ts"
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"@kunkun/api": "jsr:@kunkun/api@^0.1.0",
|
||||||
|
"@openai/openai": "jsr:@openai/openai@^4.83.0",
|
||||||
|
"@std/assert": "jsr:@std/assert@1",
|
||||||
|
"openai": "npm:openai@^4.83.0"
|
||||||
|
}
|
||||||
|
}
|
1595
deno-src/deno.lock
generated
Normal file
1595
deno-src/deno.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
deno-src/index.ts
Normal file
23
deno-src/index.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { expose } from '@kunkun/api/runtime/deno';
|
||||||
|
import OpenAI from '@openai/openai';
|
||||||
|
import type { API } from '../src/api.types';
|
||||||
|
|
||||||
|
expose({
|
||||||
|
transcribe: async (filepath: string, language: string) => {
|
||||||
|
const apiKey = Deno.env.get('OPENAI_API_KEY');
|
||||||
|
const openai = new OpenAI({ apiKey });
|
||||||
|
|
||||||
|
const fileData = await Deno.readFile(filepath);
|
||||||
|
|
||||||
|
// Convert to a File (filename is required)
|
||||||
|
const file = new File([fileData], 'audio.m4a', { type: 'audio/m4a' });
|
||||||
|
|
||||||
|
const transcription = await openai.audio.transcriptions.create({
|
||||||
|
file: file,
|
||||||
|
model: 'whisper-1',
|
||||||
|
language: language // this is optional but helps the model
|
||||||
|
});
|
||||||
|
|
||||||
|
return transcription.text;
|
||||||
|
}
|
||||||
|
} satisfies API);
|
19
deno-src/main.ts
Normal file
19
deno-src/main.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import OpenAI from 'openai';
|
||||||
|
|
||||||
|
const apiKey = Deno.env.get('OPENAI_API_KEY');
|
||||||
|
const openai = new OpenAI({ apiKey });
|
||||||
|
|
||||||
|
// expect a audio.m4a file in the current directory
|
||||||
|
const fileData = await Deno.readFile('./audio.m4a');
|
||||||
|
|
||||||
|
// Convert to a File (filename is required)
|
||||||
|
const file = new File([fileData], 'audio.m4a', { type: 'audio/m4a' });
|
||||||
|
|
||||||
|
const transcription = await openai.audio.transcriptions.create({
|
||||||
|
file: file,
|
||||||
|
model: 'whisper-1',
|
||||||
|
language: 'en' // this is optional but helps the model
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(transcription);
|
||||||
|
Deno.writeTextFileSync('transcription.txt', transcription.text);
|
33
eslint.config.js
Normal file
33
eslint.config.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import ts from 'typescript-eslint';
|
||||||
|
import svelte from 'eslint-plugin-svelte';
|
||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
|
/** @type {import('eslint').Linter.Config[]} */
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
...ts.configs.recommended,
|
||||||
|
...svelte.configs['flat/recommended'],
|
||||||
|
prettier,
|
||||||
|
...svelte.configs['flat/prettier'],
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.svelte'],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
parser: ts.parser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: ['build/', '.svelte-kit/', 'dist/']
|
||||||
|
}
|
||||||
|
];
|
116
package.json
Normal file
116
package.json
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.kunkun.sh",
|
||||||
|
"name": "kunkun-ext-speech-to-text",
|
||||||
|
"version": "0.0.7",
|
||||||
|
"license": "MIT",
|
||||||
|
"kunkun": {
|
||||||
|
"name": "Speech to Text",
|
||||||
|
"shortDescription": "Turn audio file into text",
|
||||||
|
"longDescription": "Turn audio file into text",
|
||||||
|
"identifier": "kunkun-ext-speech-to-text",
|
||||||
|
"icon": {
|
||||||
|
"type": "iconify",
|
||||||
|
"value": "arcticons:live-transcribe"
|
||||||
|
},
|
||||||
|
"demoImages": [],
|
||||||
|
"permissions": [
|
||||||
|
"event:drag-drop",
|
||||||
|
"clipboard:write-text",
|
||||||
|
"shell:kill",
|
||||||
|
"shell:stdin-write",
|
||||||
|
{
|
||||||
|
"permission": "shell:deno:spawn",
|
||||||
|
"allow": [
|
||||||
|
{
|
||||||
|
"path": "$EXTENSION/deno-src/index.ts",
|
||||||
|
"env": [
|
||||||
|
"OPENAI_API_KEY",
|
||||||
|
"OPENAI_BASE_URL",
|
||||||
|
"OPENAI_ORG_ID",
|
||||||
|
"OPENAI_PROJECT_ID",
|
||||||
|
"DEBUG"
|
||||||
|
],
|
||||||
|
"read": "*",
|
||||||
|
"net": [
|
||||||
|
"api.openai.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"permission": "fs:exists",
|
||||||
|
"allow": [
|
||||||
|
{
|
||||||
|
"path": "**"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dialog:all"
|
||||||
|
],
|
||||||
|
"customUiCmds": [
|
||||||
|
{
|
||||||
|
"main": "/",
|
||||||
|
"dist": "build",
|
||||||
|
"devMain": "http://localhost:5173",
|
||||||
|
"name": "Speech to Text",
|
||||||
|
"cmds": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"main": "preferences",
|
||||||
|
"dist": "build",
|
||||||
|
"devMain": "http://localhost:5173/preferences",
|
||||||
|
"name": "Speech to Text (Preferences)",
|
||||||
|
"cmds": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"templateUiCmds": []
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"lint": "prettier --check . && eslint .",
|
||||||
|
"format": "prettier --write ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@kksh/api": "0.1.0",
|
||||||
|
"@kksh/svelte5": "0.1.15",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-svelte": "^0.469.0",
|
||||||
|
"mode-watcher": "^0.5.0",
|
||||||
|
"openai": "^4.83.0",
|
||||||
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"tailwind-variants": "^0.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-auto": "^3.3.1",
|
||||||
|
"@sveltejs/kit": "^2.15.2",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
|
"svelte": "^5.16.6",
|
||||||
|
"svelte-check": "^4.1.1",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.0.7",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
|
"@types/eslint": "^9.6.1",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^9.17.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-svelte": "^2.46.1",
|
||||||
|
"globals": "^15.14.0",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"prettier-plugin-svelte": "^3.3.2",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript-eslint": "^8.19.1"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"build",
|
||||||
|
".gitignore"
|
||||||
|
],
|
||||||
|
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
|
||||||
|
}
|
4998
pnpm-lock.yaml
generated
Normal file
4998
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
};
|
3
src/api.types.ts
Normal file
3
src/api.types.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface API {
|
||||||
|
transcribe(filepath: string, language: string): Promise<string>;
|
||||||
|
}
|
80
src/app.css
Normal file
80
src/app.css
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
@import url('@kksh/svelte5/themes');
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 0 0% 3.9%;
|
||||||
|
|
||||||
|
--muted: 0 0% 96.1%;
|
||||||
|
--muted-foreground: 0 0% 45.1%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 0 0% 3.9%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 0 0% 3.9%;
|
||||||
|
|
||||||
|
--border: 0 0% 89.8%;
|
||||||
|
--input: 0 0% 89.8%;
|
||||||
|
|
||||||
|
--primary: 0 0% 9%;
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--secondary: 0 0% 96.1%;
|
||||||
|
--secondary-foreground: 0 0% 9%;
|
||||||
|
|
||||||
|
--accent: 0 0% 96.1%;
|
||||||
|
--accent-foreground: 0 0% 9%;
|
||||||
|
|
||||||
|
--destructive: 0 72.2% 50.6%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--ring: 0 0% 3.9%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 0 0% 3.9%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--muted: 0 0% 14.9%;
|
||||||
|
--muted-foreground: 0 0% 63.9%;
|
||||||
|
|
||||||
|
--popover: 0 0% 3.9%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--card: 0 0% 3.9%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--border: 0 0% 14.9%;
|
||||||
|
--input: 0 0% 14.9%;
|
||||||
|
|
||||||
|
--primary: 0 0% 98%;
|
||||||
|
--primary-foreground: 0 0% 9%;
|
||||||
|
|
||||||
|
--secondary: 0 0% 14.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--accent: 0 0% 14.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--ring: 0 0% 83.1%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
13
src/app.d.ts
vendored
Normal file
13
src/app.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
12
src/app.html
Normal file
12
src/app.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
20
src/lib/components/ThemeCustomizer.svelte
Normal file
20
src/lib/components/ThemeCustomizer.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ThemeCustomizerButton, type ThemeConfig, updateTheme } from '@kksh/svelte5';
|
||||||
|
import { ui } from '@kksh/api/ui/iframe';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
let config: ThemeConfig = {
|
||||||
|
radius: 0.5,
|
||||||
|
theme: 'zinc',
|
||||||
|
lightMode: 'auto'
|
||||||
|
};
|
||||||
|
onMount(() => {
|
||||||
|
ui.getTheme().then((theme) => {
|
||||||
|
config = theme;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$: updateTheme(config);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ThemeCustomizerButton bind:config />
|
48
src/lib/components/app-sidebar.svelte
Normal file
48
src/lib/components/app-sidebar.svelte
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ui } from '@kksh/api/ui/iframe';
|
||||||
|
import { Sidebar } from '@kksh/svelte5';
|
||||||
|
import { DatabaseIcon, InfoIcon } from 'lucide-svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
ui.showBackButton('bottom-right');
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
title: 'Transcribe',
|
||||||
|
url: '/',
|
||||||
|
icon: DatabaseIcon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Preferences',
|
||||||
|
url: '/preferences',
|
||||||
|
icon: InfoIcon
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sidebar.Root>
|
||||||
|
<Sidebar.Content>
|
||||||
|
<Sidebar.Group>
|
||||||
|
<Sidebar.GroupLabel data-kunkun-drag-region>Menu</Sidebar.GroupLabel>
|
||||||
|
<Sidebar.GroupContent>
|
||||||
|
<Sidebar.Menu>
|
||||||
|
{#each items as item (item.title)}
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<Sidebar.MenuButton>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a href={item.url} {...props}>
|
||||||
|
<!-- <Icon icon={item.icon} /> -->
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
{/each}
|
||||||
|
</Sidebar.Menu>
|
||||||
|
</Sidebar.GroupContent>
|
||||||
|
</Sidebar.Group>
|
||||||
|
</Sidebar.Content>
|
||||||
|
</Sidebar.Root>
|
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
56
src/lib/utils.ts
Normal file
56
src/lib/utils.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { type ClassValue, clsx } from 'clsx';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import { cubicOut } from 'svelte/easing';
|
||||||
|
import type { TransitionConfig } from 'svelte/transition';
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlyAndScaleParams = {
|
||||||
|
y?: number;
|
||||||
|
x?: number;
|
||||||
|
start?: number;
|
||||||
|
duration?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const flyAndScale = (
|
||||||
|
node: Element,
|
||||||
|
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
|
||||||
|
): TransitionConfig => {
|
||||||
|
const style = getComputedStyle(node);
|
||||||
|
const transform = style.transform === 'none' ? '' : style.transform;
|
||||||
|
|
||||||
|
const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => {
|
||||||
|
const [minA, maxA] = scaleA;
|
||||||
|
const [minB, maxB] = scaleB;
|
||||||
|
|
||||||
|
const percentage = (valueA - minA) / (maxA - minA);
|
||||||
|
const valueB = percentage * (maxB - minB) + minB;
|
||||||
|
|
||||||
|
return valueB;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styleToString = (style: Record<string, number | string | undefined>): string => {
|
||||||
|
return Object.keys(style).reduce((str, key) => {
|
||||||
|
if (style[key] === undefined) return str;
|
||||||
|
return str + `${key}:${style[key]};`;
|
||||||
|
}, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
duration: params.duration ?? 200,
|
||||||
|
delay: 0,
|
||||||
|
css: (t) => {
|
||||||
|
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
|
||||||
|
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
|
||||||
|
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
|
||||||
|
|
||||||
|
return styleToString({
|
||||||
|
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
|
||||||
|
opacity: t
|
||||||
|
});
|
||||||
|
},
|
||||||
|
easing: cubicOut
|
||||||
|
};
|
||||||
|
};
|
36
src/routes/+layout.svelte
Normal file
36
src/routes/+layout.svelte
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<script>
|
||||||
|
import '../app.css';
|
||||||
|
import { ModeWatcher } from 'mode-watcher';
|
||||||
|
import { ThemeWrapper, updateTheme } from '@kksh/svelte5';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { ui } from '@kksh/api/ui/iframe';
|
||||||
|
import { Sidebar } from '@kksh/svelte5';
|
||||||
|
import AppSidebar from '$lib/components/app-sidebar.svelte';
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
ui.registerDragRegion();
|
||||||
|
ui.getTheme().then((theme) => {
|
||||||
|
updateTheme(theme);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
ui.goBack();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ModeWatcher />
|
||||||
|
<ThemeWrapper>
|
||||||
|
<Sidebar.Provider>
|
||||||
|
<AppSidebar />
|
||||||
|
<main class="w-full">
|
||||||
|
<Sidebar.Trigger />
|
||||||
|
{@render children?.()}
|
||||||
|
</main>
|
||||||
|
</Sidebar.Provider>
|
||||||
|
</ThemeWrapper>
|
2
src/routes/+layout.ts
Normal file
2
src/routes/+layout.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const prerender = true;
|
||||||
|
export const ssr = false;
|
132
src/routes/+page.svelte
Normal file
132
src/routes/+page.svelte
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { Button, Input, Label, Textarea } from '@kksh/svelte5';
|
||||||
|
import { ui, event, fs, dialog, shell, kv, clipboard, toast } from '@kksh/api/ui/iframe';
|
||||||
|
import type { API } from '../api.types';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
let filepath = $state('');
|
||||||
|
let openaiKey = $state('');
|
||||||
|
let transcription = $state('');
|
||||||
|
let transcribing = $state(false);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
kv.get('OPENAI_API_KEY').then((key) => {
|
||||||
|
if (!key) {
|
||||||
|
toast.warning('Please enter your OpenAI API key');
|
||||||
|
return goto('/preferences');
|
||||||
|
}
|
||||||
|
openaiKey = key;
|
||||||
|
});
|
||||||
|
event.onDragDrop((payload) => {
|
||||||
|
payload.paths;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getAPI() {
|
||||||
|
const { rpcChannel, process, command } = await shell.createDenoRpcChannel<object, API>(
|
||||||
|
'$EXTENSION/deno-src/index.ts',
|
||||||
|
[],
|
||||||
|
{
|
||||||
|
allowEnv: [
|
||||||
|
'OPENAI_API_KEY',
|
||||||
|
'OPENAI_BASE_URL',
|
||||||
|
'OPENAI_ORG_ID',
|
||||||
|
'OPENAI_PROJECT_ID',
|
||||||
|
'DEBUG'
|
||||||
|
],
|
||||||
|
allowNet: ['api.openai.com'],
|
||||||
|
allowRead: [filepath],
|
||||||
|
env: {
|
||||||
|
OPENAI_API_KEY: openaiKey
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return { rpcChannel, process, command, api: rpcChannel.getAPI() };
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFile() {
|
||||||
|
dialog
|
||||||
|
.open({
|
||||||
|
directory: false,
|
||||||
|
filters: [
|
||||||
|
{ extensions: ['mp3', 'mp4', 'mpeg', 'mpga', 'm4a', 'wav', 'webm'], name: 'Audio Files' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.then((path: string) => {
|
||||||
|
if (!path) {
|
||||||
|
return toast.error('No file selected');
|
||||||
|
}
|
||||||
|
filepath = path;
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
toast.error('Failed to pick file', { description: err.message });
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transcribe() {
|
||||||
|
if (!filepath) {
|
||||||
|
return toast.error('No file selected');
|
||||||
|
}
|
||||||
|
await fs.exists(filepath).then((exists) => {
|
||||||
|
if (!exists) {
|
||||||
|
return toast.error('File does not exist');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
toast.info('Transcribing...');
|
||||||
|
const rpc = await getAPI();
|
||||||
|
if (!rpc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { api, process } = rpc;
|
||||||
|
transcribing = true;
|
||||||
|
return api
|
||||||
|
.transcribe(filepath, 'en')
|
||||||
|
.then((text) => {
|
||||||
|
console.log(text);
|
||||||
|
transcription = text;
|
||||||
|
toast.success('Transcription completed');
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Failed to transcribe', { description: err.message });
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
process.kill();
|
||||||
|
transcribing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main class="container flex flex-col gap-2">
|
||||||
|
<Label>Pick an Audio File</Label>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<Input bind:value={filepath} />
|
||||||
|
<Button onclick={pickFile}>Pick File</Button>
|
||||||
|
</div>
|
||||||
|
<Button disabled={transcribing || !filepath} class="w-full" onclick={transcribe}>
|
||||||
|
Transcribe
|
||||||
|
</Button>
|
||||||
|
{#if transcribing}
|
||||||
|
<h2 class="text-center">Transcribing...</h2>
|
||||||
|
{:else}
|
||||||
|
<Textarea bind:value={transcription} class="min-h-64 w-full" />
|
||||||
|
<Button
|
||||||
|
class="w-full"
|
||||||
|
disabled={!transcription}
|
||||||
|
onclick={() => {
|
||||||
|
clipboard
|
||||||
|
.writeText(transcription)
|
||||||
|
.then(() => {
|
||||||
|
toast.success('Copied to clipboard');
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Failed to copy to clipboard', { description: err.message });
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</main>
|
40
src/routes/preferences/+page.svelte
Normal file
40
src/routes/preferences/+page.svelte
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<script>
|
||||||
|
import { Alert, Button, ThemeWrapper, Input, Label } from '@kksh/svelte5';
|
||||||
|
import { ui, kv, toast } from '@kksh/api/ui/iframe';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
let apiKey = $state('');
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
kv.get('OPENAI_API_KEY').then((key) => {
|
||||||
|
console.log('key', key);
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
return toast.warning('Please enter your OpenAI API key');
|
||||||
|
}
|
||||||
|
apiKey = key;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
return kv
|
||||||
|
.set('OPENAI_API_KEY', apiKey)
|
||||||
|
.then(() => {
|
||||||
|
toast.success('API Key saved');
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Failed to save API Key', { description: err.message });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<h1 class="text-2xl font-bold">Preferences</h1>
|
||||||
|
<small class="block ">
|
||||||
|
OpenAI's whisper model is used to transcribe audio files.
|
||||||
|
</small>
|
||||||
|
<Label>OpenAI API Key</Label>
|
||||||
|
<form class="flex gap-1" onsubmit={save}>
|
||||||
|
<Input bind:value={apiKey} />
|
||||||
|
<Button type="submit">Save</Button>
|
||||||
|
</form>
|
||||||
|
</main>
|
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
21
svelte.config.js
Normal file
21
svelte.config.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-static';
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
|
||||||
|
kit: {
|
||||||
|
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||||
|
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||||
|
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||||
|
adapter: adapter({}),
|
||||||
|
alias: {
|
||||||
|
'@/*': './src/lib/*'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
67
tailwind.config.ts
Normal file
67
tailwind.config.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { fontFamily } from 'tailwindcss/defaultTheme';
|
||||||
|
import type { Config } from 'tailwindcss';
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
darkMode: ['class'],
|
||||||
|
content: [
|
||||||
|
'./src/**/*.{html,js,svelte,ts}',
|
||||||
|
'node_modules/@kksh/svelte5/dist/**/*.{html,js,svelte,ts}'
|
||||||
|
],
|
||||||
|
safelist: ['dark'],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: '2rem',
|
||||||
|
screens: {
|
||||||
|
'2xl': '1400px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: 'hsl(var(--border) / <alpha-value>)',
|
||||||
|
input: 'hsl(var(--input) / <alpha-value>)',
|
||||||
|
ring: 'hsl(var(--ring) / <alpha-value>)',
|
||||||
|
background: 'hsl(var(--background) / <alpha-value>)',
|
||||||
|
foreground: 'hsl(var(--foreground) / <alpha-value>)',
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
|
||||||
|
foreground: 'hsl(var(--primary-foreground) / <alpha-value>)'
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)'
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive) / <alpha-value>)',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground) / <alpha-value>)'
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted) / <alpha-value>)',
|
||||||
|
foreground: 'hsl(var(--muted-foreground) / <alpha-value>)'
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent) / <alpha-value>)',
|
||||||
|
foreground: 'hsl(var(--accent-foreground) / <alpha-value>)'
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover) / <alpha-value>)',
|
||||||
|
foreground: 'hsl(var(--popover-foreground) / <alpha-value>)'
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: 'hsl(var(--card) / <alpha-value>)',
|
||||||
|
foreground: 'hsl(var(--card-foreground) / <alpha-value>)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)'
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: [...fontFamily.sans]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||||
|
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
|
||||||
|
//
|
||||||
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
|
}
|
6
vite.config.ts
Normal file
6
vite.config.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()]
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user