chunker ui redo

This commit is contained in:
2026-03-15 16:03:53 -03:00
parent d5a3372d6b
commit b40bd68411
62 changed files with 5460 additions and 1493 deletions

24
ui/common/api/graphql.ts Normal file
View File

@@ -0,0 +1,24 @@
/**
* Shared GraphQL client for all MPR UI apps.
*/
const GRAPHQL_URL = "/api/graphql";
export async function gql<T>(
query: string,
variables?: Record<string, unknown>,
): Promise<T> {
const response = await fetch(GRAPHQL_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, variables }),
});
const json = await response.json();
if (json.errors?.length) {
throw new Error(json.errors[0].message);
}
return json.data as T;
}

View File

@@ -0,0 +1,95 @@
// @generated by protobuf-ts 2.11.1
// @generated from protobuf file "worker.proto" (package "mpr.worker", syntax proto3)
// tslint:disable
//
// Protocol Buffer Definitions - GENERATED FILE
//
// Do not edit directly. Regenerate using modelgen.
//
import type { RpcTransport } from "@protobuf-ts/runtime-rpc";
import type { ServiceInfo } from "@protobuf-ts/runtime-rpc";
import { WorkerService } from "./worker";
import type { ChunkPipelineEvent } from "./worker";
import type { ChunkStreamRequest } from "./worker";
import type { WorkerStatus } from "./worker";
import type { Empty } from "./worker";
import type { CancelResponse } from "./worker";
import type { CancelRequest } from "./worker";
import type { ProgressUpdate } from "./worker";
import type { ProgressRequest } from "./worker";
import type { ServerStreamingCall } from "@protobuf-ts/runtime-rpc";
import { stackIntercept } from "@protobuf-ts/runtime-rpc";
import type { JobResponse } from "./worker";
import type { JobRequest } from "./worker";
import type { UnaryCall } from "@protobuf-ts/runtime-rpc";
import type { RpcOptions } from "@protobuf-ts/runtime-rpc";
/**
* @generated from protobuf service mpr.worker.WorkerService
*/
export interface IWorkerServiceClient {
/**
* @generated from protobuf rpc: SubmitJob
*/
submitJob(input: JobRequest, options?: RpcOptions): UnaryCall<JobRequest, JobResponse>;
/**
* @generated from protobuf rpc: StreamProgress
*/
streamProgress(input: ProgressRequest, options?: RpcOptions): ServerStreamingCall<ProgressRequest, ProgressUpdate>;
/**
* @generated from protobuf rpc: CancelJob
*/
cancelJob(input: CancelRequest, options?: RpcOptions): UnaryCall<CancelRequest, CancelResponse>;
/**
* @generated from protobuf rpc: GetWorkerStatus
*/
getWorkerStatus(input: Empty, options?: RpcOptions): UnaryCall<Empty, WorkerStatus>;
/**
* @generated from protobuf rpc: StreamChunkPipeline
*/
streamChunkPipeline(input: ChunkStreamRequest, options?: RpcOptions): ServerStreamingCall<ChunkStreamRequest, ChunkPipelineEvent>;
}
/**
* @generated from protobuf service mpr.worker.WorkerService
*/
export class WorkerServiceClient implements IWorkerServiceClient, ServiceInfo {
typeName = WorkerService.typeName;
methods = WorkerService.methods;
options = WorkerService.options;
constructor(private readonly _transport: RpcTransport) {
}
/**
* @generated from protobuf rpc: SubmitJob
*/
submitJob(input: JobRequest, options?: RpcOptions): UnaryCall<JobRequest, JobResponse> {
const method = this.methods[0], opt = this._transport.mergeOptions(options);
return stackIntercept<JobRequest, JobResponse>("unary", this._transport, method, opt, input);
}
/**
* @generated from protobuf rpc: StreamProgress
*/
streamProgress(input: ProgressRequest, options?: RpcOptions): ServerStreamingCall<ProgressRequest, ProgressUpdate> {
const method = this.methods[1], opt = this._transport.mergeOptions(options);
return stackIntercept<ProgressRequest, ProgressUpdate>("serverStreaming", this._transport, method, opt, input);
}
/**
* @generated from protobuf rpc: CancelJob
*/
cancelJob(input: CancelRequest, options?: RpcOptions): UnaryCall<CancelRequest, CancelResponse> {
const method = this.methods[2], opt = this._transport.mergeOptions(options);
return stackIntercept<CancelRequest, CancelResponse>("unary", this._transport, method, opt, input);
}
/**
* @generated from protobuf rpc: GetWorkerStatus
*/
getWorkerStatus(input: Empty, options?: RpcOptions): UnaryCall<Empty, WorkerStatus> {
const method = this.methods[3], opt = this._transport.mergeOptions(options);
return stackIntercept<Empty, WorkerStatus>("unary", this._transport, method, opt, input);
}
/**
* @generated from protobuf rpc: StreamChunkPipeline
*/
streamChunkPipeline(input: ChunkStreamRequest, options?: RpcOptions): ServerStreamingCall<ChunkStreamRequest, ChunkPipelineEvent> {
const method = this.methods[4], opt = this._transport.mergeOptions(options);
return stackIntercept<ChunkStreamRequest, ChunkPipelineEvent>("serverStreaming", this._transport, method, opt, input);
}
}

View File

@@ -0,0 +1,946 @@
// @generated by protobuf-ts 2.11.1
// @generated from protobuf file "worker.proto" (package "mpr.worker", syntax proto3)
// tslint:disable
//
// Protocol Buffer Definitions - GENERATED FILE
//
// Do not edit directly. Regenerate using modelgen.
//
import { ServiceType } from "@protobuf-ts/runtime-rpc";
import type { BinaryWriteOptions } from "@protobuf-ts/runtime";
import type { IBinaryWriter } from "@protobuf-ts/runtime";
import { WireType } from "@protobuf-ts/runtime";
import type { BinaryReadOptions } from "@protobuf-ts/runtime";
import type { IBinaryReader } from "@protobuf-ts/runtime";
import { UnknownFieldHandler } from "@protobuf-ts/runtime";
import type { PartialMessage } from "@protobuf-ts/runtime";
import { reflectionMergePartial } from "@protobuf-ts/runtime";
import { MessageType } from "@protobuf-ts/runtime";
/**
* @generated from protobuf message mpr.worker.JobRequest
*/
export interface JobRequest {
/**
* @generated from protobuf field: string job_id = 1
*/
jobId: string;
/**
* @generated from protobuf field: string source_path = 2
*/
sourcePath: string;
/**
* @generated from protobuf field: string output_path = 3
*/
outputPath: string;
/**
* @generated from protobuf field: string preset_json = 4
*/
presetJson: string;
/**
* @generated from protobuf field: optional float trim_start = 5
*/
trimStart?: number;
/**
* @generated from protobuf field: optional float trim_end = 6
*/
trimEnd?: number;
}
/**
* @generated from protobuf message mpr.worker.JobResponse
*/
export interface JobResponse {
/**
* @generated from protobuf field: string job_id = 1
*/
jobId: string;
/**
* @generated from protobuf field: bool accepted = 2
*/
accepted: boolean;
/**
* @generated from protobuf field: string message = 3
*/
message: string;
}
/**
* @generated from protobuf message mpr.worker.ProgressRequest
*/
export interface ProgressRequest {
/**
* @generated from protobuf field: string job_id = 1
*/
jobId: string;
}
/**
* @generated from protobuf message mpr.worker.ProgressUpdate
*/
export interface ProgressUpdate {
/**
* @generated from protobuf field: string job_id = 1
*/
jobId: string;
/**
* @generated from protobuf field: int32 progress = 2
*/
progress: number;
/**
* @generated from protobuf field: int32 current_frame = 3
*/
currentFrame: number;
/**
* @generated from protobuf field: float current_time = 4
*/
currentTime: number;
/**
* @generated from protobuf field: float speed = 5
*/
speed: number;
/**
* @generated from protobuf field: string status = 6
*/
status: string;
/**
* @generated from protobuf field: optional string error = 7
*/
error?: string;
}
/**
* @generated from protobuf message mpr.worker.CancelRequest
*/
export interface CancelRequest {
/**
* @generated from protobuf field: string job_id = 1
*/
jobId: string;
}
/**
* @generated from protobuf message mpr.worker.CancelResponse
*/
export interface CancelResponse {
/**
* @generated from protobuf field: string job_id = 1
*/
jobId: string;
/**
* @generated from protobuf field: bool cancelled = 2
*/
cancelled: boolean;
/**
* @generated from protobuf field: string message = 3
*/
message: string;
}
/**
* @generated from protobuf message mpr.worker.WorkerStatus
*/
export interface WorkerStatus {
/**
* @generated from protobuf field: bool available = 1
*/
available: boolean;
/**
* @generated from protobuf field: int32 active_jobs = 2
*/
activeJobs: number;
/**
* @generated from protobuf field: repeated string supported_codecs = 3
*/
supportedCodecs: string[];
/**
* @generated from protobuf field: bool gpu_available = 4
*/
gpuAvailable: boolean;
}
/**
* Empty
*
* @generated from protobuf message mpr.worker.Empty
*/
export interface Empty {
}
/**
* @generated from protobuf message mpr.worker.ChunkStreamRequest
*/
export interface ChunkStreamRequest {
/**
* @generated from protobuf field: string job_id = 1
*/
jobId: string;
}
/**
* @generated from protobuf message mpr.worker.ChunkPipelineEvent
*/
export interface ChunkPipelineEvent {
/**
* @generated from protobuf field: string job_id = 1
*/
jobId: string;
/**
* @generated from protobuf field: string event_type = 2
*/
eventType: string;
/**
* @generated from protobuf field: int32 sequence = 3
*/
sequence: number;
/**
* @generated from protobuf field: string worker_id = 4
*/
workerId: string;
/**
* @generated from protobuf field: string state = 5
*/
state: string;
/**
* @generated from protobuf field: int32 queue_size = 6
*/
queueSize: number;
/**
* @generated from protobuf field: float elapsed = 7
*/
elapsed: number;
/**
* @generated from protobuf field: float throughput_mbps = 8
*/
throughputMbps: number;
/**
* @generated from protobuf field: int32 total_chunks = 9
*/
totalChunks: number;
/**
* @generated from protobuf field: int32 processed_chunks = 10
*/
processedChunks: number;
/**
* @generated from protobuf field: int32 failed_chunks = 11
*/
failedChunks: number;
/**
* @generated from protobuf field: string error = 12
*/
error: string;
/**
* @generated from protobuf field: float processing_time = 13
*/
processingTime: number;
/**
* @generated from protobuf field: int32 retries = 14
*/
retries: number;
}
// @generated message type with reflection information, may provide speed optimized methods
class JobRequest$Type extends MessageType<JobRequest> {
constructor() {
super("mpr.worker.JobRequest", [
{ no: 1, name: "job_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "source_path", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "output_path", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 4, name: "preset_json", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 5, name: "trim_start", kind: "scalar", opt: true, T: 2 /*ScalarType.FLOAT*/ },
{ no: 6, name: "trim_end", kind: "scalar", opt: true, T: 2 /*ScalarType.FLOAT*/ }
]);
}
create(value?: PartialMessage<JobRequest>): JobRequest {
const message = globalThis.Object.create((this.messagePrototype!));
message.jobId = "";
message.sourcePath = "";
message.outputPath = "";
message.presetJson = "";
if (value !== undefined)
reflectionMergePartial<JobRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JobRequest): JobRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string job_id */ 1:
message.jobId = reader.string();
break;
case /* string source_path */ 2:
message.sourcePath = reader.string();
break;
case /* string output_path */ 3:
message.outputPath = reader.string();
break;
case /* string preset_json */ 4:
message.presetJson = reader.string();
break;
case /* optional float trim_start */ 5:
message.trimStart = reader.float();
break;
case /* optional float trim_end */ 6:
message.trimEnd = reader.float();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: JobRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string job_id = 1; */
if (message.jobId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.jobId);
/* string source_path = 2; */
if (message.sourcePath !== "")
writer.tag(2, WireType.LengthDelimited).string(message.sourcePath);
/* string output_path = 3; */
if (message.outputPath !== "")
writer.tag(3, WireType.LengthDelimited).string(message.outputPath);
/* string preset_json = 4; */
if (message.presetJson !== "")
writer.tag(4, WireType.LengthDelimited).string(message.presetJson);
/* optional float trim_start = 5; */
if (message.trimStart !== undefined)
writer.tag(5, WireType.Bit32).float(message.trimStart);
/* optional float trim_end = 6; */
if (message.trimEnd !== undefined)
writer.tag(6, WireType.Bit32).float(message.trimEnd);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message mpr.worker.JobRequest
*/
export const JobRequest = new JobRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class JobResponse$Type extends MessageType<JobResponse> {
constructor() {
super("mpr.worker.JobResponse", [
{ no: 1, name: "job_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "accepted", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
{ no: 3, name: "message", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<JobResponse>): JobResponse {
const message = globalThis.Object.create((this.messagePrototype!));
message.jobId = "";
message.accepted = false;
message.message = "";
if (value !== undefined)
reflectionMergePartial<JobResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: JobResponse): JobResponse {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string job_id */ 1:
message.jobId = reader.string();
break;
case /* bool accepted */ 2:
message.accepted = reader.bool();
break;
case /* string message */ 3:
message.message = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: JobResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string job_id = 1; */
if (message.jobId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.jobId);
/* bool accepted = 2; */
if (message.accepted !== false)
writer.tag(2, WireType.Varint).bool(message.accepted);
/* string message = 3; */
if (message.message !== "")
writer.tag(3, WireType.LengthDelimited).string(message.message);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message mpr.worker.JobResponse
*/
export const JobResponse = new JobResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class ProgressRequest$Type extends MessageType<ProgressRequest> {
constructor() {
super("mpr.worker.ProgressRequest", [
{ no: 1, name: "job_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<ProgressRequest>): ProgressRequest {
const message = globalThis.Object.create((this.messagePrototype!));
message.jobId = "";
if (value !== undefined)
reflectionMergePartial<ProgressRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ProgressRequest): ProgressRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string job_id */ 1:
message.jobId = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: ProgressRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string job_id = 1; */
if (message.jobId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.jobId);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message mpr.worker.ProgressRequest
*/
export const ProgressRequest = new ProgressRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class ProgressUpdate$Type extends MessageType<ProgressUpdate> {
constructor() {
super("mpr.worker.ProgressUpdate", [
{ no: 1, name: "job_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "progress", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 3, name: "current_frame", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 4, name: "current_time", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ },
{ no: 5, name: "speed", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ },
{ no: 6, name: "status", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 7, name: "error", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<ProgressUpdate>): ProgressUpdate {
const message = globalThis.Object.create((this.messagePrototype!));
message.jobId = "";
message.progress = 0;
message.currentFrame = 0;
message.currentTime = 0;
message.speed = 0;
message.status = "";
if (value !== undefined)
reflectionMergePartial<ProgressUpdate>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ProgressUpdate): ProgressUpdate {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string job_id */ 1:
message.jobId = reader.string();
break;
case /* int32 progress */ 2:
message.progress = reader.int32();
break;
case /* int32 current_frame */ 3:
message.currentFrame = reader.int32();
break;
case /* float current_time */ 4:
message.currentTime = reader.float();
break;
case /* float speed */ 5:
message.speed = reader.float();
break;
case /* string status */ 6:
message.status = reader.string();
break;
case /* optional string error */ 7:
message.error = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: ProgressUpdate, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string job_id = 1; */
if (message.jobId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.jobId);
/* int32 progress = 2; */
if (message.progress !== 0)
writer.tag(2, WireType.Varint).int32(message.progress);
/* int32 current_frame = 3; */
if (message.currentFrame !== 0)
writer.tag(3, WireType.Varint).int32(message.currentFrame);
/* float current_time = 4; */
if (message.currentTime !== 0)
writer.tag(4, WireType.Bit32).float(message.currentTime);
/* float speed = 5; */
if (message.speed !== 0)
writer.tag(5, WireType.Bit32).float(message.speed);
/* string status = 6; */
if (message.status !== "")
writer.tag(6, WireType.LengthDelimited).string(message.status);
/* optional string error = 7; */
if (message.error !== undefined)
writer.tag(7, WireType.LengthDelimited).string(message.error);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message mpr.worker.ProgressUpdate
*/
export const ProgressUpdate = new ProgressUpdate$Type();
// @generated message type with reflection information, may provide speed optimized methods
class CancelRequest$Type extends MessageType<CancelRequest> {
constructor() {
super("mpr.worker.CancelRequest", [
{ no: 1, name: "job_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<CancelRequest>): CancelRequest {
const message = globalThis.Object.create((this.messagePrototype!));
message.jobId = "";
if (value !== undefined)
reflectionMergePartial<CancelRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: CancelRequest): CancelRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string job_id */ 1:
message.jobId = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: CancelRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string job_id = 1; */
if (message.jobId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.jobId);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message mpr.worker.CancelRequest
*/
export const CancelRequest = new CancelRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class CancelResponse$Type extends MessageType<CancelResponse> {
constructor() {
super("mpr.worker.CancelResponse", [
{ no: 1, name: "job_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "cancelled", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
{ no: 3, name: "message", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<CancelResponse>): CancelResponse {
const message = globalThis.Object.create((this.messagePrototype!));
message.jobId = "";
message.cancelled = false;
message.message = "";
if (value !== undefined)
reflectionMergePartial<CancelResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: CancelResponse): CancelResponse {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string job_id */ 1:
message.jobId = reader.string();
break;
case /* bool cancelled */ 2:
message.cancelled = reader.bool();
break;
case /* string message */ 3:
message.message = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: CancelResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string job_id = 1; */
if (message.jobId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.jobId);
/* bool cancelled = 2; */
if (message.cancelled !== false)
writer.tag(2, WireType.Varint).bool(message.cancelled);
/* string message = 3; */
if (message.message !== "")
writer.tag(3, WireType.LengthDelimited).string(message.message);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message mpr.worker.CancelResponse
*/
export const CancelResponse = new CancelResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class WorkerStatus$Type extends MessageType<WorkerStatus> {
constructor() {
super("mpr.worker.WorkerStatus", [
{ no: 1, name: "available", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
{ no: 2, name: "active_jobs", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 3, name: "supported_codecs", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ },
{ no: 4, name: "gpu_available", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }
]);
}
create(value?: PartialMessage<WorkerStatus>): WorkerStatus {
const message = globalThis.Object.create((this.messagePrototype!));
message.available = false;
message.activeJobs = 0;
message.supportedCodecs = [];
message.gpuAvailable = false;
if (value !== undefined)
reflectionMergePartial<WorkerStatus>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: WorkerStatus): WorkerStatus {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* bool available */ 1:
message.available = reader.bool();
break;
case /* int32 active_jobs */ 2:
message.activeJobs = reader.int32();
break;
case /* repeated string supported_codecs */ 3:
message.supportedCodecs.push(reader.string());
break;
case /* bool gpu_available */ 4:
message.gpuAvailable = reader.bool();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: WorkerStatus, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* bool available = 1; */
if (message.available !== false)
writer.tag(1, WireType.Varint).bool(message.available);
/* int32 active_jobs = 2; */
if (message.activeJobs !== 0)
writer.tag(2, WireType.Varint).int32(message.activeJobs);
/* repeated string supported_codecs = 3; */
for (let i = 0; i < message.supportedCodecs.length; i++)
writer.tag(3, WireType.LengthDelimited).string(message.supportedCodecs[i]);
/* bool gpu_available = 4; */
if (message.gpuAvailable !== false)
writer.tag(4, WireType.Varint).bool(message.gpuAvailable);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message mpr.worker.WorkerStatus
*/
export const WorkerStatus = new WorkerStatus$Type();
// @generated message type with reflection information, may provide speed optimized methods
class Empty$Type extends MessageType<Empty> {
constructor() {
super("mpr.worker.Empty", []);
}
create(value?: PartialMessage<Empty>): Empty {
const message = globalThis.Object.create((this.messagePrototype!));
if (value !== undefined)
reflectionMergePartial<Empty>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Empty): Empty {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: Empty, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message mpr.worker.Empty
*/
export const Empty = new Empty$Type();
// @generated message type with reflection information, may provide speed optimized methods
class ChunkStreamRequest$Type extends MessageType<ChunkStreamRequest> {
constructor() {
super("mpr.worker.ChunkStreamRequest", [
{ no: 1, name: "job_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<ChunkStreamRequest>): ChunkStreamRequest {
const message = globalThis.Object.create((this.messagePrototype!));
message.jobId = "";
if (value !== undefined)
reflectionMergePartial<ChunkStreamRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ChunkStreamRequest): ChunkStreamRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string job_id */ 1:
message.jobId = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: ChunkStreamRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string job_id = 1; */
if (message.jobId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.jobId);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message mpr.worker.ChunkStreamRequest
*/
export const ChunkStreamRequest = new ChunkStreamRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class ChunkPipelineEvent$Type extends MessageType<ChunkPipelineEvent> {
constructor() {
super("mpr.worker.ChunkPipelineEvent", [
{ no: 1, name: "job_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "event_type", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "sequence", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 4, name: "worker_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 5, name: "state", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 6, name: "queue_size", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 7, name: "elapsed", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ },
{ no: 8, name: "throughput_mbps", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ },
{ no: 9, name: "total_chunks", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 10, name: "processed_chunks", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 11, name: "failed_chunks", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 12, name: "error", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 13, name: "processing_time", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ },
{ no: 14, name: "retries", kind: "scalar", T: 5 /*ScalarType.INT32*/ }
]);
}
create(value?: PartialMessage<ChunkPipelineEvent>): ChunkPipelineEvent {
const message = globalThis.Object.create((this.messagePrototype!));
message.jobId = "";
message.eventType = "";
message.sequence = 0;
message.workerId = "";
message.state = "";
message.queueSize = 0;
message.elapsed = 0;
message.throughputMbps = 0;
message.totalChunks = 0;
message.processedChunks = 0;
message.failedChunks = 0;
message.error = "";
message.processingTime = 0;
message.retries = 0;
if (value !== undefined)
reflectionMergePartial<ChunkPipelineEvent>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ChunkPipelineEvent): ChunkPipelineEvent {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string job_id */ 1:
message.jobId = reader.string();
break;
case /* string event_type */ 2:
message.eventType = reader.string();
break;
case /* int32 sequence */ 3:
message.sequence = reader.int32();
break;
case /* string worker_id */ 4:
message.workerId = reader.string();
break;
case /* string state */ 5:
message.state = reader.string();
break;
case /* int32 queue_size */ 6:
message.queueSize = reader.int32();
break;
case /* float elapsed */ 7:
message.elapsed = reader.float();
break;
case /* float throughput_mbps */ 8:
message.throughputMbps = reader.float();
break;
case /* int32 total_chunks */ 9:
message.totalChunks = reader.int32();
break;
case /* int32 processed_chunks */ 10:
message.processedChunks = reader.int32();
break;
case /* int32 failed_chunks */ 11:
message.failedChunks = reader.int32();
break;
case /* string error */ 12:
message.error = reader.string();
break;
case /* float processing_time */ 13:
message.processingTime = reader.float();
break;
case /* int32 retries */ 14:
message.retries = reader.int32();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: ChunkPipelineEvent, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string job_id = 1; */
if (message.jobId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.jobId);
/* string event_type = 2; */
if (message.eventType !== "")
writer.tag(2, WireType.LengthDelimited).string(message.eventType);
/* int32 sequence = 3; */
if (message.sequence !== 0)
writer.tag(3, WireType.Varint).int32(message.sequence);
/* string worker_id = 4; */
if (message.workerId !== "")
writer.tag(4, WireType.LengthDelimited).string(message.workerId);
/* string state = 5; */
if (message.state !== "")
writer.tag(5, WireType.LengthDelimited).string(message.state);
/* int32 queue_size = 6; */
if (message.queueSize !== 0)
writer.tag(6, WireType.Varint).int32(message.queueSize);
/* float elapsed = 7; */
if (message.elapsed !== 0)
writer.tag(7, WireType.Bit32).float(message.elapsed);
/* float throughput_mbps = 8; */
if (message.throughputMbps !== 0)
writer.tag(8, WireType.Bit32).float(message.throughputMbps);
/* int32 total_chunks = 9; */
if (message.totalChunks !== 0)
writer.tag(9, WireType.Varint).int32(message.totalChunks);
/* int32 processed_chunks = 10; */
if (message.processedChunks !== 0)
writer.tag(10, WireType.Varint).int32(message.processedChunks);
/* int32 failed_chunks = 11; */
if (message.failedChunks !== 0)
writer.tag(11, WireType.Varint).int32(message.failedChunks);
/* string error = 12; */
if (message.error !== "")
writer.tag(12, WireType.LengthDelimited).string(message.error);
/* float processing_time = 13; */
if (message.processingTime !== 0)
writer.tag(13, WireType.Bit32).float(message.processingTime);
/* int32 retries = 14; */
if (message.retries !== 0)
writer.tag(14, WireType.Varint).int32(message.retries);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message mpr.worker.ChunkPipelineEvent
*/
export const ChunkPipelineEvent = new ChunkPipelineEvent$Type();
/**
* @generated ServiceType for protobuf service mpr.worker.WorkerService
*/
export const WorkerService = new ServiceType("mpr.worker.WorkerService", [
{ name: "SubmitJob", options: {}, I: JobRequest, O: JobResponse },
{ name: "StreamProgress", serverStreaming: true, options: {}, I: ProgressRequest, O: ProgressUpdate },
{ name: "CancelJob", options: {}, I: CancelRequest, O: CancelResponse },
{ name: "GetWorkerStatus", options: {}, I: Empty, O: WorkerStatus },
{ name: "StreamChunkPipeline", serverStreaming: true, options: {}, I: ChunkStreamRequest, O: ChunkPipelineEvent }
]);

42
ui/common/api/media.ts Normal file
View File

@@ -0,0 +1,42 @@
/**
* Shared media API functions — identical across all MPR UI apps.
*/
import type { MediaAsset } from "../types/generated";
import { gql } from "./graphql";
/** Fetch all media assets. */
export async function getAssets(): Promise<MediaAsset[]> {
const data = await gql<{ assets: MediaAsset[] }>(`
query {
assets {
id filename file_path status error_message file_size duration
video_codec audio_codec width height framerate bitrate
properties comments tags created_at updated_at
}
}
`);
return data.assets;
}
/** Scan media/in/ folder for new files. */
export async function scanMediaFolder(): Promise<{
found: number;
registered: number;
skipped: number;
files: string[];
}> {
const data = await gql<{
scan_media_folder: {
found: number;
registered: number;
skipped: number;
files: string[];
};
}>(`
mutation {
scan_media_folder { found registered skipped files }
}
`);
return data.scan_media_folder;
}

View File

@@ -0,0 +1,97 @@
.file-manager {
margin-bottom: 1rem;
}
.fm-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.fm-header h2 {
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
}
.fm-scan-btn {
padding: 0.25rem 0.6rem;
background: var(--bg-input);
color: var(--text-secondary);
font-size: var(--font-size-xs);
}
.fm-scan-btn:hover:not(:disabled) {
color: var(--text-primary);
background: var(--border-light);
}
.fm-list {
list-style: none;
max-height: 200px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-primary);
}
.fm-empty {
padding: 1rem;
text-align: center;
color: var(--text-muted);
font-size: var(--font-size-sm);
}
.fm-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.4rem 0.6rem;
border-bottom: 1px solid var(--border);
transition: background 0.1s;
}
.fm-item:last-child {
border-bottom: none;
}
.fm-clickable {
cursor: pointer;
}
.fm-clickable:hover {
background: var(--bg-input);
}
.fm-selected {
background: var(--accent) !important;
color: #fff;
}
.fm-selected .fm-meta {
color: rgba(255, 255, 255, 0.7);
}
.fm-item-info {
display: flex;
flex-direction: column;
gap: 0.15rem;
overflow: hidden;
min-width: 0;
}
.fm-filename {
font-size: var(--font-size-sm);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fm-meta {
font-size: var(--font-size-xs);
color: var(--text-muted);
}
.fm-actions {
flex-shrink: 0;
margin-left: 0.5rem;
}

View File

@@ -0,0 +1,84 @@
/**
* FileManager — pluggable file browser for S3/MinIO files.
*
* Handles both input file selection and output file listing.
* Used by timeline (assets + output), chunker (assets + chunk output),
* and future tools.
*/
import type { ReactNode } from "react";
import { formatSize } from "../utils/format";
import "./FileManager.css";
export interface FileEntry {
key: string;
name: string;
size?: number;
meta?: string;
}
interface FileManagerProps {
title: string;
files: FileEntry[];
selectedKey?: string | null;
onSelect?: (file: FileEntry) => void;
onScan?: () => void;
scanning?: boolean;
emptyMessage?: string;
renderActions?: (file: FileEntry) => ReactNode;
disabled?: boolean;
}
export function FileManager({
title,
files,
selectedKey,
onSelect,
onScan,
scanning = false,
emptyMessage = "No files",
renderActions,
disabled = false,
}: FileManagerProps) {
return (
<div className="file-manager">
<div className="fm-header">
<h2>{title}</h2>
{onScan && (
<button
className="fm-scan-btn"
onClick={onScan}
disabled={scanning || disabled}
>
{scanning ? "Scanning..." : "Scan Folder"}
</button>
)}
</div>
<ul className="fm-list">
{files.length === 0 ? (
<li className="fm-empty">{emptyMessage}</li>
) : (
files.map((file) => (
<li
key={file.key}
className={`fm-item ${selectedKey === file.key ? "fm-selected" : ""} ${onSelect && !disabled ? "fm-clickable" : ""}`}
onClick={() => onSelect && !disabled && onSelect(file)}
title={file.name}
>
<div className="fm-item-info">
<span className="fm-filename">{file.name}</span>
<span className="fm-meta">
{file.size != null && formatSize(file.size)}
{file.meta && (file.size != null ? ` · ${file.meta}` : file.meta)}
</span>
</div>
{renderActions && (
<div className="fm-actions">{renderActions(file)}</div>
)}
</li>
))
)}
</ul>
</div>
);
}

View File

@@ -0,0 +1,33 @@
/**
* StatusDot — small colored indicator for connection/state.
*/
const STATE_COLORS: Record<string, string> = {
connected: "var(--success)",
idle: "var(--text-muted)",
processing: "var(--processing)",
stopped: "var(--text-muted)",
error: "var(--error)",
done: "var(--success)",
};
interface StatusDotProps {
state: string;
glow?: boolean;
}
export function StatusDot({ state, glow = false }: StatusDotProps) {
const color = STATE_COLORS[state] || "var(--text-muted)";
return (
<span
style={{
display: "inline-block",
width: 8,
height: 8,
borderRadius: "50%",
background: color,
boxShadow: glow ? `0 0 6px ${color}` : undefined,
}}
/>
);
}

109
ui/common/styles/theme.css Normal file
View File

@@ -0,0 +1,109 @@
/**
* MPR Shared Theme — CSS custom properties + base styles.
* Import from any UI app: @import "../../common/styles/theme.css";
*/
:root {
--bg-primary: #0f0f0f;
--bg-panel: #1a1a1a;
--bg-surface: #141414;
--bg-input: #2a2a2a;
--border: #2a2a2a;
--border-light: #333;
--text-primary: #e0e0e0;
--text-secondary: #999;
--text-muted: #666;
--accent: #3b82f6;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
--processing: #3b82f6;
--radius: 8px;
--radius-sm: 4px;
--font-mono: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Fira Code", monospace, sans-serif;
--font-size: 14px;
--font-size-sm: 0.8rem;
--font-size-xs: 0.75rem;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-mono);
background: var(--bg-primary);
color: var(--text-primary);
font-size: var(--font-size);
}
/* Scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border-light);
border-radius: 3px;
}
/* Shared button base */
button {
cursor: pointer;
border: none;
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: var(--font-size-sm);
transition: opacity 0.15s;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Shared input base */
input,
select {
font-family: var(--font-mono);
font-size: var(--font-size-sm);
background: var(--bg-input);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 0.4rem 0.5rem;
}
input:focus,
select:focus {
outline: none;
border-color: var(--accent);
}
/* Panel base */
.panel {
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
position: relative;
}
.panel-header h2 {
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
}

View File

@@ -0,0 +1,170 @@
/**
* TypeScript Types - GENERATED FILE
*
* Do not edit directly. Regenerate using modelgen.
*/
export type AssetStatus = "pending" | "ready" | "error";
export type JobStatus = "pending" | "processing" | "completed" | "failed" | "cancelled";
export type ChunkJobStatus = "pending" | "chunking" | "processing" | "collecting" | "completed" | "failed" | "cancelled";
export interface MediaAsset {
id: string;
filename: string;
file_path: string;
status: AssetStatus;
error_message: string | null;
file_size: number | null;
duration: number | null;
video_codec: string | null;
audio_codec: string | null;
width: number | null;
height: number | null;
framerate: number | null;
bitrate: number | null;
properties: Record<string, unknown>;
comments: string;
tags: string[];
created_at: string | null;
updated_at: string | null;
}
export interface TranscodePreset {
id: string;
name: string;
description: string;
is_builtin: boolean;
container: string;
video_codec: string;
video_bitrate: string | null;
video_crf: number | null;
video_preset: string | null;
resolution: string | null;
framerate: number | null;
audio_codec: string;
audio_bitrate: string | null;
audio_channels: number | null;
audio_samplerate: number | null;
extra_args: string[];
created_at: string | null;
updated_at: string | null;
}
export interface TranscodeJob {
id: string;
source_asset_id: string;
preset_id: string | null;
preset_snapshot: Record<string, unknown>;
trim_start: number | null;
trim_end: number | null;
output_filename: string;
output_path: string | null;
output_asset_id: string | null;
status: JobStatus;
progress: number;
current_frame: number | null;
current_time: number | null;
speed: string | null;
error_message: string | null;
celery_task_id: string | null;
execution_arn: string | null;
priority: number;
created_at: string | null;
started_at: string | null;
completed_at: string | null;
}
export interface ChunkJob {
id: string;
source_asset_id: string;
chunk_duration: number;
num_workers: number;
max_retries: number;
processor_type: string;
status: ChunkJobStatus;
progress: number;
total_chunks: number;
processed_chunks: number;
failed_chunks: number;
retry_count: number;
error_message: string | null;
throughput_mbps: number | null;
elapsed_seconds: number | null;
celery_task_id: string | null;
priority: number;
created_at: string | null;
started_at: string | null;
completed_at: string | null;
}
export interface CreateJobRequest {
source_asset_id: string;
preset_id: string | null;
trim_start: number | null;
trim_end: number | null;
output_filename: string | null;
priority: number;
}
export interface UpdateAssetRequest {
comments: string | null;
tags: string[] | null;
}
export interface SystemStatus {
status: string;
version: string;
}
export interface ScanResult {
found: number;
registered: number;
skipped: number;
files: string[];
}
export interface DeleteResult {
ok: boolean;
}
export interface WorkerStatus {
available: boolean;
active_jobs: number;
supported_codecs: string[];
gpu_available: boolean;
}
export interface ChunkEvent {
sequence: number;
status: string;
size: number | null;
worker_id: string | null;
processing_time: number | null;
error: string | null;
retries: number;
}
export interface WorkerEvent {
worker_id: string;
state: string;
current_chunk: number | null;
processed: number;
errors: number;
retries: number;
}
export interface PipelineStats {
total_chunks: number;
processed: number;
failed: number;
retries: number;
elapsed: number;
throughput_mbps: number;
queue_size: number;
}
export interface ChunkOutputFile {
key: string;
size: number;
url: string;
}

21
ui/common/utils/format.ts Normal file
View File

@@ -0,0 +1,21 @@
/**
* Shared formatting utilities.
*/
export function formatSize(bytes: number | null | undefined): string {
if (!bytes) return "—";
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
if (bytes < 1024 * 1024 * 1024)
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
export function formatDuration(seconds: number | null | undefined): string {
if (!seconds) return "—";
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0)
return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
return `${m}:${s.toString().padStart(2, "0")}`;
}