@lovelybunch/api 1.0.71-alpha.4 → 1.0.71-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/routes/api/v1/context/images/index.d.ts +2 -0
- package/dist/routes/api/v1/context/images/index.js +2 -0
- package/dist/routes/api/v1/context/images/route.d.ts +3 -0
- package/dist/routes/api/v1/context/images/route.js +253 -0
- package/dist/routes/api/v1/context/index.js +2 -0
- package/dist/routes/api/v1/resources/generate-audio/index.d.ts +3 -0
- package/dist/routes/api/v1/resources/generate-audio/index.js +5 -0
- package/dist/routes/api/v1/resources/generate-audio/route.d.ts +19 -0
- package/dist/routes/api/v1/resources/generate-audio/route.js +129 -0
- package/dist/routes/api/v1/resources/generate-video/index.d.ts +3 -0
- package/dist/routes/api/v1/resources/generate-video/index.js +5 -0
- package/dist/routes/api/v1/resources/generate-video/route.d.ts +19 -0
- package/dist/routes/api/v1/resources/generate-video/route.js +235 -0
- package/dist/routes/api/v1/resources/index.js +4 -0
- package/dist/routes/api/v1/resources/route.js +3 -3
- package/package.json +4 -4
- package/static/assets/{AgentDetailPage-BkWRFbPj.js → AgentDetailPage-Bw8wHgla.js} +1 -1
- package/static/assets/{AgentEditPage-DN0TouUG.js → AgentEditPage-CQ9Y3ez8.js} +1 -1
- package/static/assets/{AgentsPage-DW6eV7oU.js → AgentsPage-DcgU1MPa.js} +1 -1
- package/static/assets/{AgentsSettingsPage-BJKnn0R0.js → AgentsSettingsPage-CgoXxqGL.js} +1 -1
- package/static/assets/{ApiKeysSettingsPage-ChFu9Eo6.js → ApiKeysSettingsPage-BgqGOa4p.js} +3 -3
- package/static/assets/{ArchitectureEditPage-DWrvSS5U.js → ArchitectureEditPage-DrEnTloL.js} +1 -1
- package/static/assets/{ArchitecturePage-BdQG779E.js → ArchitecturePage-Dpd6cyyT.js} +1 -1
- package/static/assets/{AuthSettingsPage-BXMn4xXd.js → AuthSettingsPage-wu1zFEx-.js} +2 -2
- package/static/assets/{CallbackPage-AxQqTxCV.js → CallbackPage-DYzikcPp.js} +1 -1
- package/static/assets/{CodePage-CfKZ52Zo.js → CodePage-CtM-PEHU.js} +1 -1
- package/static/assets/{CollapsibleSection-ZFq6LSXW.js → CollapsibleSection-B23j-nZD.js} +1 -1
- package/static/assets/{DashboardPage-Df-d3-fG.js → DashboardPage-BoPCq6M9.js} +1 -1
- package/static/assets/{GitPage-DAWR8rrg.js → GitPage-DYicFQBx.js} +1 -1
- package/static/assets/{GitSettingsPage-CzGuiY9j.js → GitSettingsPage-B-1Nnid8.js} +2 -2
- package/static/assets/{IdentityPage-DbsNwWtP.js → IdentityPage-Cvj6SDPj.js} +2 -2
- package/static/assets/{ImplementationStepsEditor-DZq7KcDP.js → ImplementationStepsEditor-CCfJKk2U.js} +1 -1
- package/static/assets/{IntegrationsSettingsPage-qvvsp1_G.js → IntegrationsSettingsPage-BgLXCF-u.js} +1 -1
- package/static/assets/{KnowledgeDetailPage-BMnj1qiH.js → KnowledgeDetailPage-I9erHJXN.js} +1 -1
- package/static/assets/{KnowledgeEditPage-BZa4E2M9.js → KnowledgeEditPage-BfaWotPl.js} +1 -1
- package/static/assets/{KnowledgePage-Dtq9BKZi.js → KnowledgePage-x9ShzjoT.js} +2 -2
- package/static/assets/{LoginPage-BnlCd8Cm.js → LoginPage-CZtwhbdN.js} +1 -1
- package/static/assets/{McpSettingsPage-COPMqimV.js → McpSettingsPage-BHPjMqOX.js} +1 -1
- package/static/assets/{NewAgentPage-Dtvey1oU.js → NewAgentPage-D-Pj_KgZ.js} +1 -1
- package/static/assets/{NewKnowledgePage-CbiRD2V3.js → NewKnowledgePage-CoVnCUx6.js} +1 -1
- package/static/assets/{NewProposalPage-B1tUJIZz.js → NewProposalPage-nCTq2mBC.js} +1 -1
- package/static/assets/{ProjectEditPage-CwM8LXHy.js → ProjectEditPage-d1cMZArt.js} +1 -1
- package/static/assets/{ProjectPage-BHa8M3vI.js → ProjectPage-KwwOjk5b.js} +1 -1
- package/static/assets/{PromptsSettingsPage-CwGmsIAW.js → PromptsSettingsPage-fSs6HBAk.js} +1 -1
- package/static/assets/{ProposalDetailPage-DC7NMuFi.js → ProposalDetailPage-BnLOBPPT.js} +1 -1
- package/static/assets/{ProposalEditPage-CfUidv6D.js → ProposalEditPage-bIYTFTmG.js} +1 -1
- package/static/assets/{ProposalsPage-V8ut_TsU.js → ProposalsPage-D8W2d6g6.js} +1 -1
- package/static/assets/ResourcesPage-Dq9DZFd7.js +71 -0
- package/static/assets/{RulesSettingsPage-BJ72X2Y3.js → RulesSettingsPage-SWRxKVNH.js} +1 -1
- package/static/assets/{SchedulePage-DBYxTZMb.js → SchedulePage-DGxD8ZaL.js} +1 -1
- package/static/assets/{TagInput-Cqju3cXZ.js → TagInput-CYcNHzrk.js} +1 -1
- package/static/assets/{TerminalPage-Iow1ciWG.js → TerminalPage-fabajZvk.js} +1 -1
- package/static/assets/{TerminalSessionPage-CZdzhFhY.js → TerminalSessionPage-DuJsKT9f.js} +1 -1
- package/static/assets/{UserPreferencesPage-CAuJ7yO4.js → UserPreferencesPage-DQMtobsK.js} +1 -1
- package/static/assets/{UserSettingsPage-0j7ha-RI.js → UserSettingsPage-CFEb7pD0.js} +1 -1
- package/static/assets/{UtilitiesPage-OgkcmaOS.js → UtilitiesPage-qStPGLN_.js} +1 -1
- package/static/assets/{alert-D2VTq6m-.js → alert-Baaj8mli.js} +1 -1
- package/static/assets/{arrow-down-vmWOjyjO.js → arrow-down-CSpiROUo.js} +1 -1
- package/static/assets/{arrow-left-Do2jUSS9.js → arrow-left-CdJt7_wH.js} +1 -1
- package/static/assets/{arrow-up-hMM1cdZm.js → arrow-up-ugn3lusK.js} +1 -1
- package/static/assets/{badge-D7ZZbMEG.js → badge-fHjW-wEe.js} +1 -1
- package/static/assets/{browser-modal-CWDhylx7.js → browser-modal-D6BB5mxm.js} +1 -1
- package/static/assets/{calendar-C5JtKp_F.js → calendar-DXMWQL-h.js} +1 -1
- package/static/assets/{card-D_zGBKua.js → card-C9ekeZtD.js} +1 -1
- package/static/assets/{chevron-left-JjvWea8A.js → chevron-left-BkP_F3xd.js} +1 -1
- package/static/assets/{circle-alert-DQD0CPB3.js → circle-alert-DnIYekt3.js} +1 -1
- package/static/assets/{circle-check-xj41FKuF.js → circle-check-BO-qmBFl.js} +1 -1
- package/static/assets/{circle-check-big-KfJvKA9H.js → circle-check-big-BUVglreY.js} +1 -1
- package/static/assets/{circle-play-BlGnYWUU.js → circle-play-84ZOWBDG.js} +1 -1
- package/static/assets/{circle-x-Ds97aY1x.js → circle-x-EQNlLpRR.js} +1 -1
- package/static/assets/{clipboard-D3NA8gb4.js → clipboard-Dv0iwZO9.js} +1 -1
- package/static/assets/{clock-9dU_D6sL.js → clock-BxO7xcX9.js} +1 -1
- package/static/assets/{download-CNckZYWh.js → download-CpaVC4DU.js} +1 -1
- package/static/assets/{eye-Dp6wEoME.js → eye-BSR7OEmk.js} +1 -1
- package/static/assets/{folder-git-2-B9ILjFN2.js → folder-git-2-9fWvFiHp.js} +1 -1
- package/static/assets/index-BAEEeZzi.js +458 -0
- package/static/assets/index-DdGUrKz8.css +2 -0
- package/static/assets/label-CRsu1MwQ.js +1 -0
- package/static/assets/{markdown-editor-BiM9h5iI.js → markdown-editor-Dop22vuJ.js} +1 -1
- package/static/assets/{pause-BNL49HvB.js → pause-C7KYrzQF.js} +1 -1
- package/static/assets/{play-8b83f5X0.js → play-DhUf2mKM.js} +1 -1
- package/static/assets/{plus-Be79gWKj.js → plus-CemYz5Fm.js} +1 -1
- package/static/assets/radio-group-Dz2cO4IQ.js +1 -0
- package/static/assets/{refresh-cw-C0_Ot2Zc.js → refresh-cw-BL3y7jJo.js} +1 -1
- package/static/assets/{search-CrIdR7ah.js → search-BvRo_rak.js} +1 -1
- package/static/assets/{switch-BYPAX9oF.js → switch-CDOrVL8G.js} +1 -1
- package/static/assets/{tabs-Ug7Ug5Ha.js → tabs-CP_A6r0A.js} +1 -1
- package/static/assets/{tag-BgO5mxYK.js → tag-BpNIhvRX.js} +1 -1
- package/static/assets/{terminal-preview-CDrLQbSE.js → terminal-preview-CbgoHEQH.js} +1 -1
- package/static/assets/{use-terminal-CNk4WtA6.js → use-terminal-ZvK-kdxE.js} +1 -1
- package/static/assets/{zap-cD8GAJPg.js → zap-DOtH7YrV.js} +1 -1
- package/static/index.html +2 -2
- package/static/assets/ResourcesPage-Cofgbx3H.js +0 -66
- package/static/assets/index-Bbxvuj4b.js +0 -458
- package/static/assets/index-Ca98xZVe.css +0 -2
- package/static/assets/label-CZGCmBD7.js +0 -1
- package/static/assets/radio-group-DpxssBzw.js +0 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { getLogger } from '@lovelybunch/core/logging';
|
|
5
|
+
import { requireAuth } from '../../../../../middleware/auth.js';
|
|
6
|
+
const app = new Hono();
|
|
7
|
+
// Supported image MIME types
|
|
8
|
+
const IMAGE_MIME_TYPES = {
|
|
9
|
+
'.png': 'image/png',
|
|
10
|
+
'.jpg': 'image/jpeg',
|
|
11
|
+
'.jpeg': 'image/jpeg',
|
|
12
|
+
'.gif': 'image/gif',
|
|
13
|
+
'.webp': 'image/webp',
|
|
14
|
+
'.svg': 'image/svg+xml',
|
|
15
|
+
'.ico': 'image/x-icon',
|
|
16
|
+
'.bmp': 'image/bmp',
|
|
17
|
+
};
|
|
18
|
+
const ALLOWED_EXTENSIONS = Object.keys(IMAGE_MIME_TYPES);
|
|
19
|
+
function getImagesPath() {
|
|
20
|
+
let basePath;
|
|
21
|
+
if (process.env.NODE_ENV === 'development' && process.env.GAIT_DEV_ROOT) {
|
|
22
|
+
// Dev mode: use project root .nut directory
|
|
23
|
+
basePath = process.env.GAIT_DEV_ROOT;
|
|
24
|
+
}
|
|
25
|
+
else if (process.env.GAIT_DATA_PATH) {
|
|
26
|
+
// Production mode: use GAIT_DATA_PATH (set by CLI)
|
|
27
|
+
basePath = path.resolve(process.env.GAIT_DATA_PATH, '.nut');
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// Fallback: use current directory .nut
|
|
31
|
+
basePath = path.resolve(process.cwd(), '.nut');
|
|
32
|
+
}
|
|
33
|
+
return path.join(basePath, 'context', 'images');
|
|
34
|
+
}
|
|
35
|
+
function getMimeType(filename) {
|
|
36
|
+
const ext = path.extname(filename).toLowerCase();
|
|
37
|
+
return IMAGE_MIME_TYPES[ext] || null;
|
|
38
|
+
}
|
|
39
|
+
function isValidImageFilename(filename) {
|
|
40
|
+
const ext = path.extname(filename).toLowerCase();
|
|
41
|
+
return ALLOWED_EXTENSIONS.includes(ext);
|
|
42
|
+
}
|
|
43
|
+
function sanitizeFilename(filename) {
|
|
44
|
+
// Remove path traversal attempts and invalid characters
|
|
45
|
+
return filename
|
|
46
|
+
.replace(/\.\./g, '')
|
|
47
|
+
.replace(/[/\\]/g, '')
|
|
48
|
+
.replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* GET /api/v1/context/images
|
|
52
|
+
* List all images
|
|
53
|
+
*/
|
|
54
|
+
app.get('/', async (c) => {
|
|
55
|
+
try {
|
|
56
|
+
const imagesPath = getImagesPath();
|
|
57
|
+
// Ensure directory exists
|
|
58
|
+
await fs.mkdir(imagesPath, { recursive: true });
|
|
59
|
+
const files = await fs.readdir(imagesPath);
|
|
60
|
+
const images = await Promise.all(files
|
|
61
|
+
.filter(file => isValidImageFilename(file))
|
|
62
|
+
.map(async (file) => {
|
|
63
|
+
try {
|
|
64
|
+
const filePath = path.join(imagesPath, file);
|
|
65
|
+
const stats = await fs.stat(filePath);
|
|
66
|
+
const mimeType = getMimeType(file);
|
|
67
|
+
if (!mimeType)
|
|
68
|
+
return null;
|
|
69
|
+
return {
|
|
70
|
+
filename: file,
|
|
71
|
+
size: stats.size,
|
|
72
|
+
mimeType,
|
|
73
|
+
uploadedAt: stats.mtime.toISOString(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}));
|
|
80
|
+
const validImages = images.filter((img) => img !== null);
|
|
81
|
+
return c.json({
|
|
82
|
+
success: true,
|
|
83
|
+
images: validImages.sort((a, b) => a.filename.localeCompare(b.filename))
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console.error('Error listing images:', error);
|
|
88
|
+
return c.json({ success: false, error: 'Failed to list images' }, 500);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
/**
|
|
92
|
+
* GET /api/v1/context/images/:filename
|
|
93
|
+
* Serve a specific image
|
|
94
|
+
*/
|
|
95
|
+
app.get('/:filename', async (c) => {
|
|
96
|
+
try {
|
|
97
|
+
const filename = c.req.param('filename');
|
|
98
|
+
const imagesPath = getImagesPath();
|
|
99
|
+
// Sanitize filename to prevent path traversal
|
|
100
|
+
const safeFilename = sanitizeFilename(filename);
|
|
101
|
+
if (!isValidImageFilename(safeFilename)) {
|
|
102
|
+
return c.json({ success: false, error: 'Invalid image file type' }, 400);
|
|
103
|
+
}
|
|
104
|
+
const filePath = path.join(imagesPath, safeFilename);
|
|
105
|
+
const mimeType = getMimeType(safeFilename);
|
|
106
|
+
if (!mimeType) {
|
|
107
|
+
return c.json({ success: false, error: 'Unsupported image format' }, 400);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const fileBuffer = await fs.readFile(filePath);
|
|
111
|
+
const stats = await fs.stat(filePath);
|
|
112
|
+
return new Response(fileBuffer, {
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': mimeType,
|
|
115
|
+
'Content-Length': stats.size.toString(),
|
|
116
|
+
'Cache-Control': 'public, max-age=31536000',
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error.code === 'ENOENT') {
|
|
122
|
+
return c.json({ success: false, error: 'Image not found' }, 404);
|
|
123
|
+
}
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.error('Error serving image:', error);
|
|
129
|
+
return c.json({ success: false, error: 'Failed to serve image' }, 500);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
/**
|
|
133
|
+
* POST /api/v1/context/images
|
|
134
|
+
* Upload a new image (multipart/form-data)
|
|
135
|
+
*/
|
|
136
|
+
app.post('/', async (c) => {
|
|
137
|
+
try {
|
|
138
|
+
const imagesPath = getImagesPath();
|
|
139
|
+
// Ensure directory exists
|
|
140
|
+
await fs.mkdir(imagesPath, { recursive: true });
|
|
141
|
+
// Parse multipart form data
|
|
142
|
+
const formData = await c.req.formData();
|
|
143
|
+
const file = formData.get('file');
|
|
144
|
+
if (!file || !(file instanceof File)) {
|
|
145
|
+
return c.json({ success: false, error: 'No file provided. Use multipart/form-data with a "file" field.' }, 400);
|
|
146
|
+
}
|
|
147
|
+
// Get filename from form data or use original
|
|
148
|
+
let filename = formData.get('filename')?.toString() || file.name;
|
|
149
|
+
filename = sanitizeFilename(filename);
|
|
150
|
+
if (!isValidImageFilename(filename)) {
|
|
151
|
+
return c.json({ success: false, error: `Invalid file type. Supported types: ${ALLOWED_EXTENSIONS.join(', ')}` }, 400);
|
|
152
|
+
}
|
|
153
|
+
const filePath = path.join(imagesPath, filename);
|
|
154
|
+
// Check if file already exists
|
|
155
|
+
try {
|
|
156
|
+
await fs.access(filePath);
|
|
157
|
+
return c.json({ success: false, error: 'An image with this filename already exists' }, 409);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// File doesn't exist, which is what we want
|
|
161
|
+
}
|
|
162
|
+
// Write file
|
|
163
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
164
|
+
await fs.writeFile(filePath, Buffer.from(arrayBuffer));
|
|
165
|
+
const stats = await fs.stat(filePath);
|
|
166
|
+
const mimeType = getMimeType(filename);
|
|
167
|
+
// Log image upload event
|
|
168
|
+
try {
|
|
169
|
+
const session = await requireAuth(c);
|
|
170
|
+
const actor = session ? `human:${session.email}` : 'human:unknown';
|
|
171
|
+
const logger = getLogger();
|
|
172
|
+
logger.log({
|
|
173
|
+
kind: 'image.upload',
|
|
174
|
+
actor,
|
|
175
|
+
subject: `image:${filename}`,
|
|
176
|
+
tags: ['image', 'upload'],
|
|
177
|
+
payload: {
|
|
178
|
+
filename,
|
|
179
|
+
size: stats.size,
|
|
180
|
+
mimeType,
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (logError) {
|
|
185
|
+
console.error('Error logging image upload:', logError);
|
|
186
|
+
}
|
|
187
|
+
return c.json({
|
|
188
|
+
success: true,
|
|
189
|
+
image: {
|
|
190
|
+
filename,
|
|
191
|
+
size: stats.size,
|
|
192
|
+
mimeType,
|
|
193
|
+
uploadedAt: stats.mtime.toISOString(),
|
|
194
|
+
}
|
|
195
|
+
}, 201);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
console.error('Error uploading image:', error);
|
|
199
|
+
return c.json({ success: false, error: 'Failed to upload image' }, 500);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
/**
|
|
203
|
+
* DELETE /api/v1/context/images/:filename
|
|
204
|
+
* Delete an image
|
|
205
|
+
*/
|
|
206
|
+
app.delete('/:filename', async (c) => {
|
|
207
|
+
try {
|
|
208
|
+
const filename = c.req.param('filename');
|
|
209
|
+
const imagesPath = getImagesPath();
|
|
210
|
+
// Sanitize filename to prevent path traversal
|
|
211
|
+
const safeFilename = sanitizeFilename(filename);
|
|
212
|
+
if (!isValidImageFilename(safeFilename)) {
|
|
213
|
+
return c.json({ success: false, error: 'Invalid image file type' }, 400);
|
|
214
|
+
}
|
|
215
|
+
const filePath = path.join(imagesPath, safeFilename);
|
|
216
|
+
// Check if file exists
|
|
217
|
+
try {
|
|
218
|
+
await fs.access(filePath);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return c.json({ success: false, error: 'Image not found' }, 404);
|
|
222
|
+
}
|
|
223
|
+
// Delete the file
|
|
224
|
+
await fs.unlink(filePath);
|
|
225
|
+
// Log image deletion event
|
|
226
|
+
try {
|
|
227
|
+
const session = await requireAuth(c);
|
|
228
|
+
const actor = session ? `human:${session.email}` : 'human:unknown';
|
|
229
|
+
const logger = getLogger();
|
|
230
|
+
logger.log({
|
|
231
|
+
kind: 'image.delete',
|
|
232
|
+
actor,
|
|
233
|
+
subject: `image:${safeFilename}`,
|
|
234
|
+
tags: ['image', 'delete'],
|
|
235
|
+
payload: {
|
|
236
|
+
filename: safeFilename,
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
catch (logError) {
|
|
241
|
+
console.error('Error logging image deletion:', logError);
|
|
242
|
+
}
|
|
243
|
+
return c.json({
|
|
244
|
+
success: true,
|
|
245
|
+
message: 'Image deleted successfully'
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
console.error('Error deleting image:', error);
|
|
250
|
+
return c.json({ success: false, error: 'Failed to delete image' }, 500);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
export default app;
|
|
@@ -2,8 +2,10 @@ import { Hono } from 'hono';
|
|
|
2
2
|
import project from './project/route.js';
|
|
3
3
|
import architecture from './architecture/route.js';
|
|
4
4
|
import knowledge from './knowledge/index.js';
|
|
5
|
+
import images from './images/index.js';
|
|
5
6
|
const context = new Hono();
|
|
6
7
|
context.route('/project', project);
|
|
7
8
|
context.route('/architecture', architecture);
|
|
8
9
|
context.route('/knowledge', knowledge);
|
|
10
|
+
context.route('/images', images);
|
|
9
11
|
export default context;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
export declare function POST(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
3
|
+
success: false;
|
|
4
|
+
error: {
|
|
5
|
+
code: string;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
9
|
+
success: true;
|
|
10
|
+
data: {
|
|
11
|
+
audioUrl: string;
|
|
12
|
+
};
|
|
13
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
14
|
+
success: false;
|
|
15
|
+
error: {
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
}, 500, "json">)>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import Replicate from 'replicate';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { existsSync, readFileSync } from 'fs';
|
|
5
|
+
/**
|
|
6
|
+
* Get Replicate API token from global config or environment variable
|
|
7
|
+
*/
|
|
8
|
+
function getReplicateApiToken() {
|
|
9
|
+
// First try global config
|
|
10
|
+
try {
|
|
11
|
+
const platform = process.platform;
|
|
12
|
+
let configDir;
|
|
13
|
+
if (platform === 'win32') {
|
|
14
|
+
configDir = path.join(process.env.APPDATA || homedir(), 'coconuts');
|
|
15
|
+
}
|
|
16
|
+
else if (platform === 'darwin') {
|
|
17
|
+
configDir = path.join(homedir(), 'Library', 'Application Support', 'coconuts');
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
configDir = path.join(process.env.XDG_CONFIG_HOME || path.join(homedir(), '.config'), 'coconuts');
|
|
21
|
+
}
|
|
22
|
+
const configPath = path.join(configDir, 'config.json');
|
|
23
|
+
if (existsSync(configPath)) {
|
|
24
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
25
|
+
if (config.apiKeys?.replicate) {
|
|
26
|
+
return config.apiKeys.replicate;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.warn('Failed to load Replicate token from config:', error);
|
|
32
|
+
}
|
|
33
|
+
// Fallback to environment variable
|
|
34
|
+
return process.env.REPLICATE_API_TOKEN || null;
|
|
35
|
+
}
|
|
36
|
+
// Initialize Replicate client lazily to ensure token is loaded at request time
|
|
37
|
+
function getReplicateClient() {
|
|
38
|
+
const token = getReplicateApiToken();
|
|
39
|
+
if (!token) {
|
|
40
|
+
throw new Error('Replicate API token not configured');
|
|
41
|
+
}
|
|
42
|
+
return new Replicate({
|
|
43
|
+
auth: token,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export async function POST(c) {
|
|
47
|
+
try {
|
|
48
|
+
// Check if Replicate API token is configured
|
|
49
|
+
const replicateToken = getReplicateApiToken();
|
|
50
|
+
if (!replicateToken) {
|
|
51
|
+
return c.json({
|
|
52
|
+
success: false,
|
|
53
|
+
error: {
|
|
54
|
+
code: 'MISSING_API_TOKEN',
|
|
55
|
+
message: 'Replicate API token not configured. Please add it in Settings → Integrations.'
|
|
56
|
+
}
|
|
57
|
+
}, 400);
|
|
58
|
+
}
|
|
59
|
+
const body = await c.req.json();
|
|
60
|
+
const { text, voice_id = 'English_Trustworth_Man', speed = 1, pitch = 0, emotion = 'auto', language_boost = 'English', audio_format = 'mp3' } = body;
|
|
61
|
+
if (!text || !text.trim()) {
|
|
62
|
+
return c.json({
|
|
63
|
+
success: false,
|
|
64
|
+
error: {
|
|
65
|
+
code: 'MISSING_TEXT',
|
|
66
|
+
message: 'Text is required for audio generation'
|
|
67
|
+
}
|
|
68
|
+
}, 400);
|
|
69
|
+
}
|
|
70
|
+
// Build input for minimax/speech-02-turbo
|
|
71
|
+
const input = {
|
|
72
|
+
text: text.trim(),
|
|
73
|
+
pitch,
|
|
74
|
+
speed,
|
|
75
|
+
volume: 1,
|
|
76
|
+
bitrate: 128000,
|
|
77
|
+
channel: 'mono',
|
|
78
|
+
emotion,
|
|
79
|
+
voice_id,
|
|
80
|
+
sample_rate: 32000,
|
|
81
|
+
audio_format,
|
|
82
|
+
language_boost,
|
|
83
|
+
subtitle_enable: false,
|
|
84
|
+
english_normalization: true
|
|
85
|
+
};
|
|
86
|
+
// Run the model
|
|
87
|
+
const replicateClient = getReplicateClient();
|
|
88
|
+
const output = await replicateClient.run('minimax/speech-02-turbo', { input });
|
|
89
|
+
// Extract URL from output
|
|
90
|
+
// Replicate output can be: FileOutput object with url() method
|
|
91
|
+
let audioUrl;
|
|
92
|
+
if (typeof output === 'string') {
|
|
93
|
+
audioUrl = output;
|
|
94
|
+
}
|
|
95
|
+
else if (output && typeof output === 'object') {
|
|
96
|
+
// Check for url() method (FileOutput object)
|
|
97
|
+
const outputObj = output;
|
|
98
|
+
if (typeof outputObj.url === 'function') {
|
|
99
|
+
audioUrl = outputObj.url();
|
|
100
|
+
}
|
|
101
|
+
else if ('url' in outputObj && typeof outputObj.url === 'string') {
|
|
102
|
+
audioUrl = outputObj.url;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
throw new Error('Unexpected output format from Replicate');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
throw new Error('Unexpected output format from Replicate');
|
|
110
|
+
}
|
|
111
|
+
return c.json({
|
|
112
|
+
success: true,
|
|
113
|
+
data: {
|
|
114
|
+
audioUrl
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.error('Error generating audio:', error);
|
|
120
|
+
const message = error instanceof Error ? error.message : 'Failed to generate audio';
|
|
121
|
+
return c.json({
|
|
122
|
+
success: false,
|
|
123
|
+
error: {
|
|
124
|
+
code: 'GENERATION_ERROR',
|
|
125
|
+
message
|
|
126
|
+
}
|
|
127
|
+
}, 500);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
export declare function POST(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
3
|
+
success: false;
|
|
4
|
+
error: {
|
|
5
|
+
code: string;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
9
|
+
success: true;
|
|
10
|
+
data: {
|
|
11
|
+
videoUrl: string;
|
|
12
|
+
};
|
|
13
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
14
|
+
success: false;
|
|
15
|
+
error: {
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
}, 500, "json">)>;
|