-
-
Notifications
You must be signed in to change notification settings - Fork 146
feat(editor): correct, add and time words in the transcript #570
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
a6c16a4
fbce0ca
c001278
b74565a
f33571c
492bdbb
188f365
2745da4
bef60e9
0258709
24ae842
0ea2f38
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 | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -26,6 +26,7 @@ import { | |||||||||||||||||||||||||||||||||||||||
| replaceTimeline, | ||||||||||||||||||||||||||||||||||||||||
| setClipSourceRange, | ||||||||||||||||||||||||||||||||||||||||
| } from "../../src/lib/ai-edition/document/timeline"; | ||||||||||||||||||||||||||||||||||||||||
| import { setDocumentWordText } from "../../src/lib/ai-edition/document/transcript"; | ||||||||||||||||||||||||||||||||||||||||
| import type { AxcutDocument } from "../../src/lib/ai-edition/schema"; | ||||||||||||||||||||||||||||||||||||||||
| import { hasAnyClipWithCamera } from "../../src/lib/ai-edition/timeline/camera"; | ||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -490,6 +491,18 @@ export const setCameraFullscreenArgs = z.object({ | |||||||||||||||||||||||||||||||||||||||
| endSec: secondsSchema.optional(), | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export const getTranscriptWordsArgs = z.object({ | ||||||||||||||||||||||||||||||||||||||||
| assetId: z.string().min(1).optional(), | ||||||||||||||||||||||||||||||||||||||||
| startSec: secondsSchema.optional(), | ||||||||||||||||||||||||||||||||||||||||
| endSec: secondsSchema.optional(), | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export const setWordTextArgs = z.object({ | ||||||||||||||||||||||||||||||||||||||||
| wordId: z.string().min(1), | ||||||||||||||||||||||||||||||||||||||||
| text: z.string(), | ||||||||||||||||||||||||||||||||||||||||
| assetId: z.string().min(1).optional(), | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export const removeTrimArgs = z.object({ | ||||||||||||||||||||||||||||||||||||||||
| trimRangeId: z.string().min(1), | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -525,7 +538,9 @@ export const removeClipArgs = z.object({ | |||||||||||||||||||||||||||||||||||||||
| export const OPENSCREEN_TOOL_NAMES = [ | ||||||||||||||||||||||||||||||||||||||||
| "getCurrentDocument", | ||||||||||||||||||||||||||||||||||||||||
| "getTranscript", | ||||||||||||||||||||||||||||||||||||||||
| "getTranscriptWords", | ||||||||||||||||||||||||||||||||||||||||
| "getCursorTrack", | ||||||||||||||||||||||||||||||||||||||||
| "setWordText", | ||||||||||||||||||||||||||||||||||||||||
| "addTrim", | ||||||||||||||||||||||||||||||||||||||||
| "addTrims", | ||||||||||||||||||||||||||||||||||||||||
| "setTrim", | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -592,6 +607,9 @@ export const PHANTOM_TOOL_NAMES = [ | |||||||||||||||||||||||||||||||||||||||
| * remaining surfaces (descriptions, built tools, executor cases) to each other. | ||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
| export const MUTATING_TOOL_NAMES: ReadonlySet<string> = new Set([ | ||||||||||||||||||||||||||||||||||||||||
| // Writes the transcript, not the timeline — but it writes the document, so it is a | ||||||||||||||||||||||||||||||||||||||||
| // consented edit like any other. | ||||||||||||||||||||||||||||||||||||||||
| "setWordText", | ||||||||||||||||||||||||||||||||||||||||
| "addTrim", | ||||||||||||||||||||||||||||||||||||||||
| "addTrims", | ||||||||||||||||||||||||||||||||||||||||
| "addZooms", | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1203,6 +1221,96 @@ export function executeAgentTool( | |||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // The word-level read. `getTranscript` answers in SEGMENTS, whose ids belong to a | ||||||||||||||||||||||||||||||||||||||||
| // different namespace than the words — so on its own it cannot address anything | ||||||||||||||||||||||||||||||||||||||||
| // `setWordText` takes. This is the one that can. It is separate rather than folded | ||||||||||||||||||||||||||||||||||||||||
| // in because a whole transcript is already ~70k tokens and most turns never touch a | ||||||||||||||||||||||||||||||||||||||||
| // word; the span filter is there so fixing one name costs one phrase, not the film. | ||||||||||||||||||||||||||||||||||||||||
| case "getTranscriptWords": { | ||||||||||||||||||||||||||||||||||||||||
| const parsed = getTranscriptWordsArgs.safeParse(args); | ||||||||||||||||||||||||||||||||||||||||
| if (!parsed.success) return failure(parsed.error.message); | ||||||||||||||||||||||||||||||||||||||||
| const assetId = | ||||||||||||||||||||||||||||||||||||||||
| parsed.data.assetId ?? document.project.primaryAssetId ?? document.assets[0]?.id; | ||||||||||||||||||||||||||||||||||||||||
| const transcript = | ||||||||||||||||||||||||||||||||||||||||
| document.transcripts.find((t) => t.assetId === assetId) ?? | ||||||||||||||||||||||||||||||||||||||||
| (document.transcript?.assetId === assetId ? document.transcript : null); | ||||||||||||||||||||||||||||||||||||||||
| if (!transcript) { | ||||||||||||||||||||||||||||||||||||||||
| return failure(`No transcript for asset ${assetId ?? "(none)"}.`); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| const from = parsed.data.startSec ?? Number.NEGATIVE_INFINITY; | ||||||||||||||||||||||||||||||||||||||||
| const to = parsed.data.endSec ?? Number.POSITIVE_INFINITY; | ||||||||||||||||||||||||||||||||||||||||
| const words = transcript.words | ||||||||||||||||||||||||||||||||||||||||
| .filter((word) => word.endSec >= from && word.startSec <= to) | ||||||||||||||||||||||||||||||||||||||||
| .map((word) => ({ | ||||||||||||||||||||||||||||||||||||||||
| id: word.id, | ||||||||||||||||||||||||||||||||||||||||
| text: word.text, | ||||||||||||||||||||||||||||||||||||||||
| startSec: word.startSec, | ||||||||||||||||||||||||||||||||||||||||
| endSec: word.endSec, | ||||||||||||||||||||||||||||||||||||||||
| // Only the words that are NOT plain transcription say so, so the common | ||||||||||||||||||||||||||||||||||||||||
| // case costs nothing to read. | ||||||||||||||||||||||||||||||||||||||||
| ...(word.source ? { source: word.source } : {}), | ||||||||||||||||||||||||||||||||||||||||
| ...(word.originalText !== undefined ? { originalText: word.originalText } : {}), | ||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||
| ok: true, | ||||||||||||||||||||||||||||||||||||||||
| resultJson: JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||
| assetId, | ||||||||||||||||||||||||||||||||||||||||
| language: transcript.language, | ||||||||||||||||||||||||||||||||||||||||
| total: transcript.words.length, | ||||||||||||||||||||||||||||||||||||||||
| returned: words.length, | ||||||||||||||||||||||||||||||||||||||||
| words, | ||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Correcting what the transcriber HEARD. This writes text and nothing else: the | ||||||||||||||||||||||||||||||||||||||||
| // captions follow it, the film does not move. The tool for making a spoken word go | ||||||||||||||||||||||||||||||||||||||||
| // away is addTrim, which removes its audio with it. | ||||||||||||||||||||||||||||||||||||||||
| case "setWordText": { | ||||||||||||||||||||||||||||||||||||||||
| const parsed = setWordTextArgs.safeParse(args); | ||||||||||||||||||||||||||||||||||||||||
| if (!parsed.success) return failure(parsed.error.message); | ||||||||||||||||||||||||||||||||||||||||
| const assetId = | ||||||||||||||||||||||||||||||||||||||||
| parsed.data.assetId ?? document.project.primaryAssetId ?? document.assets[0]?.id; | ||||||||||||||||||||||||||||||||||||||||
| if (!assetId) return failure("Project has no assets — nothing to correct."); | ||||||||||||||||||||||||||||||||||||||||
| const { wordId, text } = parsed.data; | ||||||||||||||||||||||||||||||||||||||||
| const transcript = document.transcripts.find((t) => t.assetId === assetId); | ||||||||||||||||||||||||||||||||||||||||
| const before = transcript?.words.find((word) => word.id === wordId); | ||||||||||||||||||||||||||||||||||||||||
| if (!before) { | ||||||||||||||||||||||||||||||||||||||||
| return failure( | ||||||||||||||||||||||||||||||||||||||||
| `No word ${wordId} in the transcript for asset ${assetId}. ` + | ||||||||||||||||||||||||||||||||||||||||
| `Call getTranscriptWords to read the ids.`, | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1276
to
+1283
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. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Report a missing transcript instead of blaming the word id.
Separate the two failures so the message names the real cause. 🛠️ Proposed fix const transcript = document.transcripts.find((t) => t.assetId === assetId);
+ if (!transcript) {
+ return failure(`No transcript for asset ${assetId} — there is nothing to correct.`);
+ }
- const before = transcript?.words.find((word) => word.id === wordId);
+ const before = transcript.words.find((word) => word.id === wordId);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| if (before.text === text) { | ||||||||||||||||||||||||||||||||||||||||
| return failure(`Word ${wordId} already reads "${text}" — nothing to change.`); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| let next: AxcutDocument; | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| next = setDocumentWordText(document, assetId, wordId, text); | ||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||
| return failure(error instanceof Error ? error.message : String(error)); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| const after = next.transcripts | ||||||||||||||||||||||||||||||||||||||||
| .find((t) => t.assetId === assetId) | ||||||||||||||||||||||||||||||||||||||||
| ?.words.find((word) => word.id === wordId); | ||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||
| ok: true, | ||||||||||||||||||||||||||||||||||||||||
| document: next, | ||||||||||||||||||||||||||||||||||||||||
| resultJson: JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||
| wordId, | ||||||||||||||||||||||||||||||||||||||||
| assetId, | ||||||||||||||||||||||||||||||||||||||||
| text: after?.text ?? text, | ||||||||||||||||||||||||||||||||||||||||
| was: before.text, | ||||||||||||||||||||||||||||||||||||||||
| // Absent once the word is back to what the transcriber said — the pair is | ||||||||||||||||||||||||||||||||||||||||
| // cleared on that round trip, and the model should be able to see it. | ||||||||||||||||||||||||||||||||||||||||
| originalText: after?.originalText, | ||||||||||||||||||||||||||||||||||||||||
| blanked: text.trim().length === 0, | ||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||
| summary: | ||||||||||||||||||||||||||||||||||||||||
| text.trim().length === 0 ? `blanked "${before.text}"` : `"${before.text}" → "${text}"`, | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| case "addTrim": { | ||||||||||||||||||||||||||||||||||||||||
| const parsed = addTrimArgs.safeParse(args); | ||||||||||||||||||||||||||||||||||||||||
| if (!parsed.success) return failure(parsed.error.message); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
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.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Normalize an inverted span before filtering.
fromandtoare used exactly as given. If the model passesstartSec: 5withendSec: 2, the predicate can never hold and the tool answersreturned: 0withok: true. The model then reads that as "no words in this passage". Every other span-taking tool in this file normalizes the pair first (seeaddTrimat Line 1323 andaddZoomat Line 1582).🛠️ Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents