copy path and chat about file/folder

This commit is contained in:
2026-02-23 05:15:13 +00:00
parent bdefc52331
commit 9acef6cf6c
24 changed files with 1052 additions and 45 deletions
+68
View File
@@ -0,0 +1,68 @@
import { createRouter } from '../../create-router';
import { settingsPath } from './server-settings';
type OcrConfig = {
url: string;
model: string;
};
export async function readOcrConfig(): Promise<OcrConfig | undefined> {
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
return settings.ocr as OcrConfig | undefined;
}
export const ocrRouter = createRouter();
ocrRouter.get('/', async (ctx) => {
const ocr = await readOcrConfig();
if (!ocr) return ctx.json(null);
return ctx.json(ocr);
});
ocrRouter.put('/', async (ctx) => {
const body = await ctx.req.json<OcrConfig>();
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
settings.ocr = body;
await Bun.write(settingsPath, JSON.stringify(settings, null, 2));
return ctx.json({ success: true });
});
ocrRouter.post('/models', async (ctx) => {
const body = await ctx.req.json<{ url: string }>();
if (!body.url) return ctx.json({ error: 'URL required' }, 400);
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const res = await fetch(`${body.url.replace(/\/+$/, '')}/v1/models`, {
signal: controller.signal,
});
clearTimeout(timeout);
if (!res.ok) return ctx.json({ error: `Server returned ${res.status}` }, 500);
const json = (await res.json()) as { data?: { id: string }[] };
const models = (json.data ?? []).map((m) => m.id);
return ctx.json({ models });
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
return ctx.json({ error: message }, 500);
}
});
ocrRouter.post('/test', async (ctx) => {
const body = await ctx.req.json<OcrConfig>();
if (!body.url) return ctx.json({ error: 'URL required' }, 400);
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const res = await fetch(`${body.url.replace(/\/+$/, '')}/v1/models`, {
signal: controller.signal,
});
clearTimeout(timeout);
if (!res.ok) return ctx.json({ error: `Server returned ${res.status}` }, 500);
return ctx.json({ success: true });
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
return ctx.json({ error: message }, 500);
}
});
@@ -10,6 +10,9 @@ import { piMonoRouter } from './pi-mono';
import { applicationsRouter } from './applications';
import { resourcesRouter } from './resources';
import { smtpRouter } from './smtp';
import { ttsRouter } from './tts';
import { sttRouter } from './stt';
import { ocrRouter } from './ocr';
const configDir = `${homedir()}/.config/officer.dev`;
export const settingsPath = `${configDir}/server-settings.json`;
@@ -28,6 +31,9 @@ serverSettingsRouter.route('/pi-mono', piMonoRouter);
serverSettingsRouter.route('/applications', applicationsRouter);
serverSettingsRouter.route('/resources', resourcesRouter);
serverSettingsRouter.route('/smtp', smtpRouter);
serverSettingsRouter.route('/tts', ttsRouter);
serverSettingsRouter.route('/stt', sttRouter);
serverSettingsRouter.route('/ocr', ocrRouter);
serverSettingsRouter.get('/settings', async (ctx) => {
const settings = await Bun.file(settingsPath).json();
+48
View File
@@ -0,0 +1,48 @@
import { createRouter } from '../../create-router';
import { settingsPath } from './server-settings';
type SttConfig = {
url: string;
};
export async function readSttConfig(): Promise<SttConfig | undefined> {
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
return settings.stt as SttConfig | undefined;
}
export const sttRouter = createRouter();
sttRouter.get('/', async (ctx) => {
const stt = await readSttConfig();
if (!stt) return ctx.json(null);
return ctx.json(stt);
});
sttRouter.put('/', async (ctx) => {
const body = await ctx.req.json<SttConfig>();
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
settings.stt = body;
await Bun.write(settingsPath, JSON.stringify(settings, null, 2));
return ctx.json({ success: true });
});
sttRouter.post('/test', async (ctx) => {
const body = await ctx.req.json<{ url: string }>();
if (!body.url) return ctx.json({ error: 'URL required' }, 400);
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const res = await fetch(`${body.url.replace(/\/+$/, '')}/inference`, {
method: 'POST',
body: new FormData(),
signal: controller.signal,
});
clearTimeout(timeout);
// Whisper will return an error for empty form, but a response means it's reachable
return ctx.json({ success: true, status: res.status });
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
return ctx.json({ error: message }, 500);
}
});
+127
View File
@@ -0,0 +1,127 @@
import { createRouter } from '../../create-router';
import { settingsPath } from './server-settings';
type TtsConfig = {
provider: 'openai' | 'elevenlabs';
url: string;
apiKey?: string;
model: string;
voice: string;
};
function maskSecret(value: string | undefined): string | undefined {
if (!value || value.length < 8) return value ? '****' : undefined;
return value.slice(0, 4) + '****' + value.slice(-4);
}
export async function readTtsConfig(): Promise<TtsConfig | undefined> {
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
return settings.tts as TtsConfig | undefined;
}
export const ttsRouter = createRouter();
ttsRouter.get('/', async (ctx) => {
const tts = await readTtsConfig();
if (!tts) return ctx.json(null);
return ctx.json({ ...tts, apiKey: maskSecret(tts.apiKey) });
});
ttsRouter.put('/', async (ctx) => {
const body = await ctx.req.json<TtsConfig>();
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
const existing: TtsConfig | undefined = settings.tts;
if (existing && body.apiKey?.includes('****')) {
body.apiKey = existing.apiKey;
}
settings.tts = body;
await Bun.write(settingsPath, JSON.stringify(settings, null, 2));
return ctx.json({ success: true });
});
ttsRouter.post('/voices', async (ctx) => {
const body = await ctx.req.json<{ provider: string; url?: string; apiKey?: string }>();
try {
if (body.provider === 'elevenlabs') {
if (!body.apiKey) return ctx.json({ error: 'API key required' }, 400);
const res = await fetch('https://api.elevenlabs.io/v1/voices', {
headers: { 'xi-api-key': body.apiKey },
});
if (!res.ok) return ctx.json({ error: `ElevenLabs error: ${res.status}` }, 500);
const json = (await res.json()) as { voices: { voice_id: string; name: string }[] };
return ctx.json({ voices: json.voices.map((v) => v.voice_id) });
}
// OpenAI-compatible
if (!body.url) return ctx.json({ error: 'URL required' }, 400);
const headers: Record<string, string> = {};
if (body.apiKey) headers['Authorization'] = `Bearer ${body.apiKey}`;
const res = await fetch(`${body.url.replace(/\/+$/, '')}/v1/audio/voices`, { headers });
if (!res.ok) return ctx.json({ error: `Voices fetch failed: ${res.status}` }, 500);
const json = (await res.json()) as { voices: string[] };
return ctx.json({ voices: json.voices });
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
return ctx.json({ error: message }, 500);
}
});
ttsRouter.post('/test', async (ctx) => {
const body = await ctx.req.json<TtsConfig>();
const settings = await Bun.file(settingsPath).json().catch(() => ({}));
const saved: TtsConfig | undefined = settings.tts;
if (saved && body.apiKey?.includes('****')) {
body.apiKey = saved.apiKey;
}
try {
if (body.provider === 'elevenlabs') {
if (!body.apiKey) return ctx.json({ error: 'API key required for ElevenLabs' }, 400);
const res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(body.voice)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'xi-api-key': body.apiKey,
},
body: JSON.stringify({
text: 'This is a test of the text to speech system.',
model_id: body.model,
}),
});
if (!res.ok) {
const text = await res.text().catch(() => '');
return ctx.json({ error: `ElevenLabs error: ${res.status} ${text}` }, 500);
}
const buffer = await res.arrayBuffer();
return new Response(buffer, { headers: { 'Content-Type': 'audio/mpeg' } });
}
// OpenAI-compatible (Kokoro, OpenAI, etc.)
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (body.apiKey) headers['Authorization'] = `Bearer ${body.apiKey}`;
const res = await fetch(`${body.url.replace(/\/+$/, '')}/v1/audio/speech`, {
method: 'POST',
headers,
body: JSON.stringify({
model: body.model,
input: 'This is a test of the text to speech system.',
voice: body.voice,
response_format: 'mp3',
}),
});
if (!res.ok) {
const text = await res.text().catch(() => '');
return ctx.json({ error: `TTS error: ${res.status} ${text}` }, 500);
}
const buffer = await res.arrayBuffer();
return new Response(buffer, { headers: { 'Content-Type': 'audio/mpeg' } });
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
return ctx.json({ error: message }, 500);
}
});