-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat: add transcript download endpoint to developer REST API (#1656) #2092
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
07a82ac
7e94c28
59b69c5
e186c9f
d745ba9
b19720b
7d9427b
e1f7254
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import { readFileSync, readdirSync, statSync } from "node:fs"; | ||
| import { join } from "node:path"; | ||
| import { describe, expect, it } from "vitest"; | ||
| import { RATE_LIMIT_IDS } from "../../lib/rate-limit"; | ||
|
|
||
| // Rate limit IDs declared in advance for firewall rules or separate app packages | ||
| // that are intentionally not yet wired in apps/web endpoints. | ||
| const UNWIRED_RATE_LIMIT_IDS = new Set([ | ||
| "AUTH_OTP_VERIFY", | ||
| "AUTH_OTP_SEND", | ||
| "LOOM_DOWNLOAD", | ||
| "MESSENGER_MESSAGE", | ||
| "DESKTOP_LOGS", | ||
| ]); | ||
|
|
||
| function getAllTsFiles(dir: string): string[] { | ||
| let results: string[] = []; | ||
| const list = readdirSync(dir); | ||
| for (const file of list) { | ||
| const filePath = join(dir, file); | ||
| const stat = statSync(filePath); | ||
| if (stat && stat.isDirectory()) { | ||
| if (file !== "node_modules" && file !== ".next" && file !== "dist") { | ||
| results = results.concat(getAllTsFiles(filePath)); | ||
| } | ||
| } else if (file.endsWith(".ts") || file.endsWith(".tsx")) { | ||
| if (!filePath.endsWith("lib/rate-limit.ts") && !filePath.endsWith("rate-limit-ids.test.ts")) { | ||
| results.push(filePath); | ||
| } | ||
| } | ||
| } | ||
| return results; | ||
| } | ||
|
|
||
| describe("RATE_LIMIT_IDS reference contract", () => { | ||
| it("ensures every active declared RATE_LIMIT_ID is referenced outside lib/rate-limit.ts", () => { | ||
| const webAppDir = join(process.cwd()); | ||
| const tsFiles = getAllTsFiles(webAppDir); | ||
|
|
||
| let combinedSource = ""; | ||
| for (const file of tsFiles) { | ||
| combinedSource += readFileSync(file, "utf8") + "\n"; | ||
| } | ||
|
|
||
| const unreferencedKeys: string[] = []; | ||
|
|
||
| for (const [key, value] of Object.entries(RATE_LIMIT_IDS)) { | ||
| if (UNWIRED_RATE_LIMIT_IDS.has(key)) { | ||
| continue; | ||
| } | ||
|
|
||
| const hasKeyRef = combinedSource.includes(`RATE_LIMIT_IDS.${key}`); | ||
| const hasValueRef = combinedSource.includes(`"${value}"`) || combinedSource.includes(`'${value}'`); | ||
|
|
||
| if (!hasKeyRef && !hasValueRef) { | ||
| unreferencedKeys.push(key); | ||
| } | ||
| } | ||
|
|
||
| expect( | ||
| unreferencedKeys, | ||
| `The following RATE_LIMIT_IDS are declared but never referenced: ${unreferencedKeys.join(", ")}`, | ||
| ).toEqual([]); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -123,3 +123,43 @@ app.get("/:id/status", async (c) => { | |
| }, | ||
| }); | ||
| }); | ||
|
|
||
| app.get("/:id/transcript", async (c) => { | ||
| const appId = c.get("developerAppId"); | ||
| const videoId = c.req.param("id"); | ||
|
|
||
| const [video] = await db() | ||
| .select() | ||
| .from(developerVideos) | ||
| .where( | ||
| and( | ||
| eq(developerVideos.id, videoId), | ||
| eq(developerVideos.appId, appId), | ||
| isNull(developerVideos.deletedAt), | ||
| ), | ||
| ) | ||
| .limit(1); | ||
|
|
||
| if (!video) { | ||
| return c.json({ error: "Video not found" }, 404); | ||
| } | ||
|
|
||
| if (video.transcriptionStatus === "PROCESSING") { | ||
| return c.json({ error: "Transcript still processing" }, 202); | ||
| } | ||
|
|
||
| if (video.transcriptionStatus !== "COMPLETE") { | ||
| return c.json( | ||
| { error: "No transcript available", status: video.transcriptionStatus }, | ||
| 400, | ||
| ); | ||
| } | ||
|
|
||
| return c.json({ | ||
| data: { | ||
| id: video.id, | ||
| transcriptionStatus: video.transcriptionStatus, | ||
| s3Key: video.s3Key, | ||
| }, | ||
| }); | ||
|
Comment on lines
+158
to
+164
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When transcription is complete, this endpoint returns Knowledge Base Used: Infra, Storage and Config Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/web/app/api/developer/v1/[...route]/videos.ts
Line: 158-164
Comment:
**Transcript response returns video key**
When transcription is complete, this endpoint returns `developerVideos.s3Key`, which identifies the raw uploaded video rather than the separately stored `transcription.vtt` object. Because the response includes neither transcript content nor a signed transcript URL, API consumers cannot download the completed transcript.
**Knowledge Base Used:** [Infra, Storage and Config](https://app.greptile.com/cap/-/custom-context/knowledge-base/capsoftware/cap/-/docs/infra-storage-config.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a valid share URL has a trailing slash or query parameters,
rsplit('/').next()produces an empty ID or retains the query string in the ID. The metadata request then receives an invalidvideoIdand returns 400 or 404 for an otherwise valid video.Knowledge Base Used: Cap CLI (
apps/cli)Prompt To Fix With AI