feat(subtitles): 补字幕第 1 步——上传字幕 / zip,识别语言与季集,配对写入视频同目录,历史与撤销
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
feee481915
commit
ef29bd62ce
22 files changed
+1554
-2
No files matched your search
@@ -0,0 +1,84 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
type SubtitleApplyRequest,
|
||||
SubtitleApplyResult,
|
||||
SubtitleParseResponse,
|
||||
SubtitleTargetsResponse,
|
||||
SubtitleWritesResponse,
|
||||
SubtitleWriteView,
|
||||
} from "@umt/contracts";
|
||||
import { api } from "~/lib/api.ts";
|
||||
import { getParsed, parseOrThrow } from "./library.ts";
|
||||
|
||||
// 补字幕(docs/plan/subtitles.md):目标视频、解析上传、写入、历史与撤销
|
||||
|
||||
export const subtitleKeys = {
|
||||
targets: (entityId: string) => ["subtitles", "targets", entityId] as const,
|
||||
writes: (entityId: string) => ["subtitles", "writes", entityId] as const,
|
||||
};
|
||||
|
||||
export function useSubtitleTargets(entityId: string, enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: subtitleKeys.targets(entityId),
|
||||
enabled,
|
||||
queryFn: ({ signal }) => getParsed(SubtitleTargetsResponse, "/subtitles/targets", { entityId }, signal),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSubtitleWrites(entityId: string, enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: subtitleKeys.writes(entityId),
|
||||
enabled,
|
||||
queryFn: ({ signal }) => getParsed(SubtitleWritesResponse, "/subtitles/writes", { entityId }, signal),
|
||||
});
|
||||
}
|
||||
|
||||
/** 读本地文件为 base64(字幕与 zip 都很小,直接放进 JSON) */
|
||||
async function fileBase64(f: File): Promise<string> {
|
||||
const buf = new Uint8Array(await f.arrayBuffer());
|
||||
let s = "";
|
||||
for (let i = 0; i < buf.length; i += 0x8000) s += String.fromCharCode(...buf.subarray(i, i + 0x8000));
|
||||
return btoa(s);
|
||||
}
|
||||
|
||||
export function useParseSubtitles() {
|
||||
return useMutation({
|
||||
mutationFn: async ({ entityId, files }: { entityId: string; files: File[] }) =>
|
||||
parseOrThrow(
|
||||
SubtitleParseResponse,
|
||||
await api.post<unknown>("/subtitles/parse", {
|
||||
entityId,
|
||||
files: await Promise.all(files.map(async (f) => ({ name: f.name, base64: await fileBase64(f) }))),
|
||||
}),
|
||||
"/subtitles/parse",
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export function useApplySubtitles() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (body: SubtitleApplyRequest) =>
|
||||
parseOrThrow(SubtitleApplyResult, await api.post<unknown>("/subtitles/apply", body), "/subtitles/apply"),
|
||||
onSuccess: (_r, body) => {
|
||||
qc.invalidateQueries({ queryKey: subtitleKeys.targets(body.entityId) });
|
||||
qc.invalidateQueries({ queryKey: subtitleKeys.writes(body.entityId) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUndoSubtitle(entityId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (id: string) =>
|
||||
parseOrThrow(
|
||||
SubtitleWriteView,
|
||||
await api.post<unknown>(`/subtitles/writes/${id}/undo`),
|
||||
"/subtitles/writes/undo",
|
||||
),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: subtitleKeys.targets(entityId) });
|
||||
qc.invalidateQueries({ queryKey: subtitleKeys.writes(entityId) });
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user