Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 222 additions & 0 deletions electron/ai-edition/agent-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ describe("the mutating-tool table", () => {
"removeClip",
"removeModifier",
"removeTrim",
"removeWords",
"replaceTimeline",
"setAnnotation",
"setAudio",
Expand Down Expand Up @@ -2384,6 +2385,227 @@ describe("getTranscriptWords", () => {
});
});

describe("removeWords", () => {
function repeatedWordDocument(): AxcutDocument {
const base = fixtureDocument();
return {
...base,
transcripts: [
{
...base.transcripts[0],
words: [
{ id: "filler", segmentId: "seg_1", startSec: 1, endSec: 1.2, text: "like" },
{ id: "meaningful", segmentId: "seg_1", startSec: 3, endSec: 3.2, text: "like" },
],
},
],
};
}

it("cuts only the selected occurrence and reports the trim actually stored", () => {
const before = repeatedWordDocument();
const read = run(before, "getTranscriptWords", {});
expect(JSON.parse(read.resultJson).words.map((word: { id: string }) => word.id)).toEqual([
"filler",
"meaningful",
]);
const result = run(before, "removeWords", { wordIds: ["filler"] });
expect(result.ok).toBe(true);
const next = result.document as AxcutDocument;
const trim = next.timeline.trimRanges.at(-1);
expect(trim).toMatchObject({ assetId: "asset_1", clipId: "clip_1", origin: "agent" });
expect(trim?.endSec).toBeLessThan(3); // the second "like" still plays
expect(next.transcripts).toEqual(before.transcripts);
expect(next.timeline.clips).toEqual(before.timeline.clips);
const payload = JSON.parse(result.resultJson);
expect(payload).toMatchObject({
requested: 1,
removedCount: 1,
removed: [
{
wordId: "filler",
text: "like",
assetId: "asset_1",
clipId: "clip_1",
wordStartSec: 1,
wordEndSec: 1.2,
trimRangeId: trim?.id,
startSec: trim?.startSec,
endSec: trim?.endSec,
},
],
});
expect(next.timeline.trimRanges).toHaveLength(before.timeline.trimRanges.length + 1);
});

it("cuts consecutive words as one span, gaps included, and the others apart", () => {
const base = fixtureDocument();
const before: AxcutDocument = {
...base,
transcripts: [
{
...base.transcripts[0],
words: [
{ id: "word_1", segmentId: "seg_1", startSec: 1, endSec: 1.2, text: "so" },
{ id: "word_2", segmentId: "seg_1", startSec: 1.4, endSec: 1.5, text: "I" },
{ id: "word_3", segmentId: "seg_1", startSec: 1.6, endSec: 1.9, text: "mean" },
{ id: "word_4", segmentId: "seg_1", startSec: 2.2, endSec: 2.4, text: "the" },
{ id: "word_5", segmentId: "seg_1", startSec: 3, endSec: 3.2, text: "um" },
],
},
],
};
const result = run(before, "removeWords", { wordIds: ["word_3", "word_5", "word_2"] });
expect(result.ok).toBe(true);
const added = result.document?.timeline.trimRanges.slice(before.timeline.trimRanges.length);
expect(added?.map(({ startSec, endSec }) => [startSec, endSec])).toEqual([
[1.4, 1.9],
[3, 3.2],
]);
const removed = JSON.parse(result.resultJson).removed;
expect(removed.map((word: { wordId: string }) => word.wordId)).toEqual([
"word_2",
"word_3",
"word_5",
]);
expect(removed[1].trimRangeId).toBe(removed[0].trimRangeId);
expect(removed[2].trimRangeId).not.toBe(removed[0].trimRangeId);
});

it("refuses a duplicated or already-cut word without cutting anything", () => {
const before = repeatedWordDocument();
const duplicated = run(before, "removeWords", { wordIds: ["filler", "filler"] });
expect(duplicated.ok).toBe(false);
expect(duplicated.document).toBeUndefined();
const cut = run(before, "removeWords", { wordIds: ["filler"] }).document as AxcutDocument;
const again = run(cut, "removeWords", { wordIds: ["meaningful", "filler"] });
expect(again.ok).toBe(false);
expect(again.document).toBeUndefined();
expect(again.resultJson).toContain("already cut");
});

it("preserves addTrim warnings when removing a filler cuts a zoom transition", () => {
const before = repeatedWordDocument();
before.timeline.trimRanges = [];
const zoomed = run(before, "addZoom", { startSec: 2, endSec: 3, depth: 3 }).document;
expect(zoomed).toBeDefined();
if (!zoomed) throw new Error("Zoom fixture failed");
const directTrim = run(zoomed, "addTrim", {
assetId: "asset_1",
clipId: "clip_1",
startSec: 1,
endSec: 1.2,
});
const expected = JSON.parse(directTrim.resultJson).cutTransitions;
expect(expected.length).toBeGreaterThan(0);
const removed = run(zoomed, "removeWords", { assetId: "asset_1", wordIds: ["filler"] });
expect(removed.ok).toBe(true);
expect(JSON.parse(removed.resultJson).cutTransitions).toEqual(expected);
});

it("refuses an invalid ID atomically, including when another ID is valid", () => {
const before = repeatedWordDocument();
const result = run(before, "removeWords", { wordIds: ["filler", "missing"] });
expect(result.ok).toBe(false);
expect(result.document).toBeUndefined();
expect(result.resultJson).toContain("Unknown transcript word ID: missing");
expect(before.timeline.trimRanges).toHaveLength(1);
});

it("resolves a word ID to its own asset and clip, not the primary asset", () => {
const before = repeatedWordDocument();
before.assets.push({ ...before.assets[0], id: "asset_2" });
before.timeline.clips.push({
...before.timeline.clips[0],
id: "clip_3",
assetId: "asset_2",
});
before.transcripts.push({
...before.transcripts[0],
assetId: "asset_2",
words: [{ id: "other_filler", segmentId: "seg_1", startSec: 4, endSec: 4.2, text: "um" }],
});
const result = run(before, "removeWords", { wordIds: ["other_filler"] });
expect(result.ok).toBe(true);
expect(result.document?.timeline.trimRanges.at(-1)).toMatchObject({
assetId: "asset_2",
clipId: "clip_3",
});
expect(JSON.parse(result.resultJson).removed[0]).toMatchObject({
assetId: "asset_2",
clipId: "clip_3",
});
});

it("qualifies normally numbered word IDs by the asset read from the transcript", () => {
const before = repeatedWordDocument();
before.transcripts[0].words[0].id = "word_1";
before.assets.push({ ...before.assets[0], id: "asset_2" });
before.timeline.clips.push({ ...before.timeline.clips[0], id: "clip_3", assetId: "asset_2" });
before.transcripts.push({
...before.transcripts[0],
assetId: "asset_2",
words: [{ id: "word_1", segmentId: "seg_1", startSec: 4, endSec: 4.2, text: "um" }],
});
const read = JSON.parse(run(before, "getTranscriptWords", { assetId: "asset_2" }).resultJson);
expect(read.words[0].id).toBe("word_1");
const result = run(before, "removeWords", {
assetId: "asset_2",
wordIds: [read.words[0].id],
});
expect(result.ok).toBe(true);
expect(result.document?.timeline.trimRanges.at(-1)).toMatchObject({
assetId: "asset_2",
clipId: "clip_3",
});
expect(
result.document?.timeline.trimRanges.filter((trim) => trim.assetId === "asset_1"),
).toEqual(before.timeline.trimRanges);
expect(result.document?.transcripts).toEqual(before.transcripts);
const ambiguous = run(before, "removeWords", { wordIds: ["word_1"] });
expect(ambiguous.ok).toBe(false);
expect(ambiguous.document).toBeUndefined();
const unknownAsset = run(before, "removeWords", {
assetId: "missing_asset",
wordIds: ["word_1"],
});
expect(unknownAsset.ok).toBe(false);
expect(unknownAsset.document).toBeUndefined();
});

it("refuses invalid timestamps without guessing a span", () => {
const before = repeatedWordDocument();
before.transcripts[0].words[0].endSec = Number.NaN;
const result = run(before, "removeWords", { wordIds: ["filler"] });
expect(result.ok).toBe(false);
expect(result.document).toBeUndefined();
expect(result.resultJson).toContain("invalid source timestamps");
});

it("refuses a word covered by two clips", () => {
const before = repeatedWordDocument();
before.timeline.clips.push({
...before.timeline.clips[0],
id: "duplicate_clip",
});
const result = run(before, "removeWords", { wordIds: ["filler"] });
expect(result.ok).toBe(false);
expect(result.document).toBeUndefined();
expect(result.resultJson).toContain("exactly one clip; found 2");
});

it("honours project edit consent", () => {
const result = executeAgentTool(
repeatedWordDocument(),
"removeWords",
JSON.stringify({ wordIds: ["filler"] }),
{ editsAllowed: false },
);
expect(result.document).toBeUndefined();
expect(JSON.parse(result.resultJson).code).toBe("consent_required");
});
});

describe("setWordText", () => {
it("changes the text and leaves the timeline alone", () => {
const before = documentWithWords();
Expand Down
Loading
Loading