feat: 实现模板片段匹配规则功能并修复数据库迁移问题

新功能:
- 为TrackSegment添加匹配规则字段,支持固定素材和AI分类两种规则
- 实现SegmentMatchingRuleEditor组件,支持在模板详情页面编辑片段匹配规则
- 添加update_segment_matching_rule和get_segment_matching_rule API接口
- 扩展前端类型定义和服务函数以支持匹配规则操作

 修复:
- 修复数据库迁移逻辑导致每次重启清空素材和轨道数据的问题
- 为模板表和轨道片段表迁移添加条件检查,只在必要时执行
- 修正matching_rule字段的默认值格式,匹配Rust枚举序列化格式
- 完善轨道片段表重建时的字段迁移逻辑

 技术改进:
- 数据库schema更新,添加matching_rule列到track_segments表
- 优化数据库迁移性能,避免不必要的表重建操作
- 增强错误处理和日志输出,便于问题排查

 文件变更:
- 后端: template_service.rs, template.rs, database.rs, template_commands.rs, lib.rs
- 前端: SegmentMatchingRuleEditor.tsx, TemplateDetailModal.tsx, templateStore.ts, template.ts
This commit is contained in:
imeepos
2025-07-15 09:43:04 +08:00
parent be8c032158
commit 05d29832b0
9 changed files with 581 additions and 71 deletions

View File

@@ -5,7 +5,7 @@ use rusqlite::{params, Row};
use crate::data::models::template::{
Template, TemplateMaterial, Track, TrackSegment, CanvasConfig,
TemplateMaterialType, TrackType, ImportStatus, UploadStatus,
CreateTemplateRequest
CreateTemplateRequest, SegmentMatchingRule
};
use crate::infrastructure::database::Database;
@@ -144,11 +144,14 @@ impl TemplateService {
// 保存轨道片段
for segment in &track.segments {
let matching_rule_json = serde_json::to_string(&segment.matching_rule)
.unwrap_or_else(|_| r#"{"FixedMaterial":{}}"#.to_string());
tx.execute(
"INSERT OR REPLACE INTO track_segments (
id, track_id, template_material_id, name, start_time, end_time,
duration, segment_index, properties, created_at, updated_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
duration, segment_index, properties, matching_rule, created_at, updated_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
params![
segment.id,
segment.track_id,
@@ -159,6 +162,7 @@ impl TemplateService {
segment.duration as i64,
segment.segment_index,
segment.properties,
matching_rule_json,
segment.created_at.to_rfc3339(),
segment.updated_at.to_rfc3339()
],
@@ -224,7 +228,7 @@ impl TemplateService {
// 查询轨道片段
let mut segment_stmt = conn.prepare(
"SELECT id, track_id, template_material_id, name, start_time, end_time,
duration, segment_index, properties, created_at, updated_at
duration, segment_index, properties, matching_rule, created_at, updated_at
FROM track_segments WHERE track_id = ?1 ORDER BY segment_index"
)?;
@@ -535,6 +539,11 @@ impl TemplateService {
/// 数据库行转换为轨道片段
fn row_to_track_segment(&self, row: &Row) -> rusqlite::Result<TrackSegment> {
// 解析匹配规则,如果解析失败则使用默认值
let matching_rule = row.get::<_, Option<String>>("matching_rule")?
.and_then(|rule_str| serde_json::from_str(&rule_str).ok())
.unwrap_or_default();
Ok(TrackSegment {
id: row.get("id")?,
track_id: row.get("track_id")?,
@@ -545,6 +554,7 @@ impl TemplateService {
duration: row.get::<_, i64>("duration")? as u64,
segment_index: row.get::<_, i64>("segment_index")? as u32,
properties: row.get("properties")?,
matching_rule,
created_at: chrono::DateTime::parse_from_rfc3339(&row.get::<_, String>("created_at")?)
.unwrap()
.with_timezone(&chrono::Utc),
@@ -588,7 +598,7 @@ impl TemplateService {
// 查询每个轨道的真实片段数据
let mut segment_stmt = conn.prepare(
"SELECT id, track_id, template_material_id, name, start_time, end_time,
duration, segment_index, properties, created_at, updated_at
duration, segment_index, properties, matching_rule, created_at, updated_at
FROM track_segments WHERE track_id = ?1 ORDER BY segment_index"
)?;
@@ -605,4 +615,54 @@ impl TemplateService {
Ok(())
}
/// 更新轨道片段的匹配规则
pub async fn update_segment_matching_rule(
&self,
segment_id: &str,
matching_rule: SegmentMatchingRule,
) -> Result<()> {
let conn = self.database.get_connection();
let conn = conn.lock().unwrap();
let matching_rule_json = serde_json::to_string(&matching_rule)
.map_err(|e| anyhow!("Failed to serialize matching rule: {}", e))?;
let updated_rows = conn.execute(
"UPDATE track_segments SET matching_rule = ?1, updated_at = ?2 WHERE id = ?3",
params![
matching_rule_json,
chrono::Utc::now().to_rfc3339(),
segment_id
],
)?;
if updated_rows == 0 {
return Err(anyhow!("Track segment not found: {}", segment_id));
}
Ok(())
}
/// 获取轨道片段的匹配规则
pub async fn get_segment_matching_rule(&self, segment_id: &str) -> Result<SegmentMatchingRule> {
let conn = self.database.get_connection();
let conn = conn.lock().unwrap();
let matching_rule_json: Option<String> = match conn.query_row(
"SELECT matching_rule FROM track_segments WHERE id = ?1",
params![segment_id],
|row| row.get(0),
) {
Ok(value) => Some(value),
Err(rusqlite::Error::QueryReturnedNoRows) => None,
Err(e) => return Err(anyhow!("Database error: {}", e)),
};
let matching_rule = matching_rule_json
.and_then(|rule_str| serde_json::from_str(&rule_str).ok())
.unwrap_or_default();
Ok(matching_rule)
}
}

View File

@@ -63,6 +63,41 @@ pub struct Track {
pub updated_at: DateTime<Utc>,
}
/// 片段匹配规则类型
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SegmentMatchingRule {
/// 固定素材(默认)- 不替换
FixedMaterial,
/// AI分类素材 - 使用指定AI分类的素材
AiClassification { category_id: String, category_name: String },
}
impl Default for SegmentMatchingRule {
fn default() -> Self {
Self::FixedMaterial
}
}
impl SegmentMatchingRule {
/// 获取规则的显示名称
pub fn display_name(&self) -> String {
match self {
Self::FixedMaterial => "固定素材".to_string(),
Self::AiClassification { category_name, .. } => format!("AI分类: {}", category_name),
}
}
/// 检查是否为固定素材
pub fn is_fixed_material(&self) -> bool {
matches!(self, Self::FixedMaterial)
}
/// 检查是否为AI分类
pub fn is_ai_classification(&self) -> bool {
matches!(self, Self::AiClassification { .. })
}
}
/// 轨道片段
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackSegment {
@@ -75,6 +110,7 @@ pub struct TrackSegment {
pub duration: u64, // 片段时长(微秒)
pub segment_index: u32, // 片段在轨道中的索引
pub properties: Option<String>, // JSON格式的片段属性如变换、效果等
pub matching_rule: SegmentMatchingRule, // 片段匹配规则
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -353,6 +389,7 @@ impl TrackSegment {
duration: end_time - start_time,
segment_index,
properties: None,
matching_rule: SegmentMatchingRule::default(), // 默认为固定素材
created_at: now,
updated_at: now,
}
@@ -362,4 +399,15 @@ impl TrackSegment {
pub fn associate_material(&mut self, material_id: String) {
self.template_material_id = Some(material_id);
}
/// 设置匹配规则
pub fn set_matching_rule(&mut self, rule: SegmentMatchingRule) {
self.matching_rule = rule;
self.updated_at = Utc::now();
}
/// 获取匹配规则的显示名称
pub fn get_matching_rule_display(&self) -> String {
self.matching_rule.display_name()
}
}

View File

@@ -331,6 +331,7 @@ impl Database {
duration INTEGER NOT NULL,
segment_index INTEGER NOT NULL,
properties TEXT,
matching_rule TEXT DEFAULT '\"FixedMaterial\"',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (track_id) REFERENCES tracks (id) ON DELETE CASCADE,
@@ -399,7 +400,13 @@ impl Database {
[],
);
// 移除模板表的 project_id 字段(如果存在)
// 检查是否需要移除模板表的 project_id 字段
// 只有在表中存在 project_id 字段时才需要重建表
let has_project_id_column = conn.prepare("SELECT project_id FROM templates LIMIT 1").is_ok();
if has_project_id_column {
println!("Migrating templates table to remove project_id column...");
// SQLite 不支持 DROP COLUMN需要重建表
let _ = conn.execute(
"CREATE TABLE IF NOT EXISTS templates_new (
@@ -434,7 +441,16 @@ impl Database {
let _ = conn.execute("ALTER TABLE templates RENAME TO templates_old", []);
let _ = conn.execute("ALTER TABLE templates_new RENAME TO templates", []);
// 修复轨道片段表的外键约束问题
println!("Templates table migration completed");
}
// 检查是否需要修复轨道片段表的外键约束问题
// 只有在表结构需要更新时才重建表
let needs_track_segments_migration = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='track_segments_old'").is_err();
if needs_track_segments_migration {
println!("Migrating track_segments table structure...");
// 重建 track_segments 表,恢复正确的 template_material_id 外键约束
let _ = conn.execute(
"CREATE TABLE IF NOT EXISTS track_segments_new (
@@ -447,6 +463,7 @@ impl Database {
duration INTEGER NOT NULL,
segment_index INTEGER NOT NULL,
properties TEXT,
matching_rule TEXT DEFAULT '\"FixedMaterial\"',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (track_id) REFERENCES tracks (id) ON DELETE CASCADE,
@@ -456,19 +473,37 @@ impl Database {
);
// 迁移轨道片段数据
// 首先检查旧表是否有 matching_rule 字段
let has_matching_rule = conn.prepare("SELECT matching_rule FROM track_segments LIMIT 1").is_ok();
if has_matching_rule {
// 如果有 matching_rule 字段,包含它在迁移中
let _ = conn.execute(
"INSERT OR IGNORE INTO track_segments_new
SELECT id, track_id, template_material_id, name, start_time, end_time,
duration, segment_index, properties, created_at, updated_at
duration, segment_index, properties, matching_rule, created_at, updated_at
FROM track_segments",
[],
);
} else {
// 如果没有 matching_rule 字段,使用默认值
let _ = conn.execute(
"INSERT OR IGNORE INTO track_segments_new
SELECT id, track_id, template_material_id, name, start_time, end_time,
duration, segment_index, properties, '\"FixedMaterial\"', created_at, updated_at
FROM track_segments",
[],
);
}
// 删除旧表并重命名新表
let _ = conn.execute("DROP TABLE IF EXISTS track_segments_old", []);
let _ = conn.execute("ALTER TABLE track_segments RENAME TO track_segments_old", []);
let _ = conn.execute("ALTER TABLE track_segments_new RENAME TO track_segments", []);
println!("Track segments table migration completed");
}
// 创建模板素材表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_template_materials_template_id ON template_materials (template_id)",
@@ -795,6 +830,17 @@ impl Database {
println!("Added model_id column and index to materials table");
}
// 添加片段匹配规则字段到轨道片段表
let has_matching_rule_column = conn.prepare("SELECT matching_rule FROM track_segments LIMIT 1").is_ok();
if !has_matching_rule_column {
println!("Adding matching_rule column to track_segments table");
conn.execute(
"ALTER TABLE track_segments ADD COLUMN matching_rule TEXT DEFAULT '\"FixedMaterial\"'",
[],
)?;
println!("Added matching_rule column to track_segments table");
}
// 暂时禁用自动清理,避免启动时卡住
// self.cleanup_invalid_projects()?;

View File

@@ -155,6 +155,8 @@ pub fn run() {
commands::template_commands::warm_template_cache,
commands::template_commands::cleanup_template_performance_data,
commands::template_commands::import_template_optimized,
commands::template_commands::update_segment_matching_rule,
commands::template_commands::get_segment_matching_rule,
// 测试命令
commands::test_commands::test_database_connection,
commands::test_commands::test_template_table,

View File

@@ -4,7 +4,7 @@ use anyhow::Result;
use crate::data::models::template::{
Template, ImportTemplateRequest, BatchImportRequest, ImportProgress,
CreateTemplateRequest
CreateTemplateRequest, SegmentMatchingRule
};
use crate::business::services::template_service::{TemplateService, TemplateQueryOptions, TemplateListResponse};
use crate::business::services::template_import_service::TemplateImportService;
@@ -338,4 +338,29 @@ pub async fn import_template_optimized(
.map_err(|e| e.to_string())
}
/// 更新轨道片段的匹配规则
#[tauri::command]
pub async fn update_segment_matching_rule(
segment_id: String,
matching_rule: SegmentMatchingRule,
database: State<'_, Arc<Database>>,
) -> Result<(), String> {
let service = TemplateService::new(database.inner().clone());
service.update_segment_matching_rule(&segment_id, matching_rule)
.await
.map_err(|e| e.to_string())
}
/// 获取轨道片段的匹配规则
#[tauri::command]
pub async fn get_segment_matching_rule(
segment_id: String,
database: State<'_, Arc<Database>>,
) -> Result<SegmentMatchingRule, String> {
let service = TemplateService::new(database.inner().clone());
service.get_segment_matching_rule(&segment_id)
.await
.map_err(|e| e.to_string())
}

View File

@@ -0,0 +1,213 @@
import React, { useState, useEffect } from 'react';
import { PencilIcon, CheckIcon, XMarkIcon } from '@heroicons/react/24/outline';
import { SegmentMatchingRule, SegmentMatchingRuleHelper } from '../../types/template';
import { AiClassification } from '../../types/aiClassification';
import { useTemplateStore } from '../../stores/templateStore';
import { CustomSelect } from '../CustomSelect';
import { AiClassificationService } from '../../services/aiClassificationService';
interface SegmentMatchingRuleEditorProps {
segmentId: string;
currentRule: SegmentMatchingRule;
onRuleUpdated?: (newRule: SegmentMatchingRule) => void;
}
/**
* 片段匹配规则编辑器组件
* 遵循前端开发规范的组件设计支持固定素材和AI分类规则的设置
*/
export const SegmentMatchingRuleEditor: React.FC<SegmentMatchingRuleEditorProps> = ({
segmentId,
currentRule,
onRuleUpdated,
}) => {
const [isEditing, setIsEditing] = useState(false);
const [editingRule, setEditingRule] = useState<SegmentMatchingRule>(currentRule);
const [aiClassifications, setAiClassifications] = useState<AiClassification[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { updateSegmentMatchingRule } = useTemplateStore();
// 加载AI分类列表
useEffect(() => {
const loadClassifications = async () => {
try {
const classifications = await AiClassificationService.getActiveClassifications();
setAiClassifications(classifications);
} catch (error) {
console.error('加载AI分类失败:', error);
}
};
if (isEditing) {
loadClassifications();
}
}, [isEditing]);
// 重置编辑状态
useEffect(() => {
setEditingRule(currentRule);
setError(null);
}, [currentRule, isEditing]);
const handleStartEdit = () => {
setIsEditing(true);
setEditingRule(currentRule);
setError(null);
};
const handleCancelEdit = () => {
setIsEditing(false);
setEditingRule(currentRule);
setError(null);
};
const handleSaveRule = async () => {
try {
setLoading(true);
setError(null);
await updateSegmentMatchingRule(segmentId, editingRule);
setIsEditing(false);
onRuleUpdated?.(editingRule);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '保存匹配规则失败';
setError(errorMessage);
} finally {
setLoading(false);
}
};
const handleRuleTypeChange = (ruleType: string) => {
if (ruleType === 'fixed') {
setEditingRule(SegmentMatchingRuleHelper.createFixedMaterial());
} else if (ruleType === 'ai_classification') {
// 如果有可用的AI分类选择第一个作为默认值
if (aiClassifications.length > 0) {
const firstClassification = aiClassifications[0];
setEditingRule(SegmentMatchingRuleHelper.createAiClassification(
firstClassification.id,
firstClassification.name
));
}
}
};
const handleAiClassificationChange = (classificationId: string) => {
const classification = aiClassifications.find(c => c.id === classificationId);
if (classification) {
setEditingRule(SegmentMatchingRuleHelper.createAiClassification(
classification.id,
classification.name
));
}
};
const getCurrentRuleType = (rule: SegmentMatchingRule): string => {
return SegmentMatchingRuleHelper.isFixedMaterial(rule) ? 'fixed' : 'ai_classification';
};
const ruleTypeOptions = [
{ value: 'fixed', label: '固定素材' },
{ value: 'ai_classification', label: 'AI分类素材' },
];
const classificationOptions = aiClassifications.map(classification => ({
value: classification.id,
label: classification.name,
}));
if (!isEditing) {
return (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<span className="text-xs font-medium text-gray-700">:</span>
<span className={`px-2 py-1 rounded text-xs ${
SegmentMatchingRuleHelper.isFixedMaterial(currentRule)
? 'bg-gray-100 text-gray-800'
: 'bg-blue-100 text-blue-800'
}`}>
{SegmentMatchingRuleHelper.getDisplayName(currentRule)}
</span>
</div>
<button
onClick={handleStartEdit}
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
title="编辑匹配规则"
>
<PencilIcon className="w-4 h-4" />
</button>
</div>
);
}
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-gray-700">:</span>
<div className="flex items-center space-x-1">
<button
onClick={handleSaveRule}
disabled={loading}
className="p-1 text-green-600 hover:text-green-800 disabled:opacity-50 transition-colors"
title="保存"
>
<CheckIcon className="w-4 h-4" />
</button>
<button
onClick={handleCancelEdit}
disabled={loading}
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50 transition-colors"
title="取消"
>
<XMarkIcon className="w-4 h-4" />
</button>
</div>
</div>
<div className="space-y-2">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
</label>
<CustomSelect
value={getCurrentRuleType(editingRule)}
onChange={handleRuleTypeChange}
options={ruleTypeOptions}
placeholder="选择规则类型"
className="text-xs"
/>
</div>
{SegmentMatchingRuleHelper.isAiClassification(editingRule) && (
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
AI分类
</label>
<CustomSelect
value={SegmentMatchingRuleHelper.getAiClassificationInfo(editingRule)?.category_id || ''}
onChange={handleAiClassificationChange}
options={classificationOptions}
placeholder="选择AI分类"
className="text-xs"
/>
</div>
)}
</div>
{error && (
<div className="text-xs text-red-600 bg-red-50 p-2 rounded">
{error}
</div>
)}
{loading && (
<div className="text-xs text-blue-600 bg-blue-50 p-2 rounded">
...
</div>
)}
</div>
);
};

View File

@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { X, Calendar, Clock, Monitor, Layers, FileText, Image, Video, Music, Type, Sparkles, CheckCircle, XCircle, AlertCircle, Upload, Cloud } from 'lucide-react';
import { Template, TemplateMaterial, TemplateMaterialType, TrackType } from '../../types/template';
import { SegmentMatchingRuleEditor } from './SegmentMatchingRuleEditor';
interface TemplateDetailModalProps {
template: Template;
@@ -539,6 +540,18 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
</div>
)}
{/* 匹配规则编辑器 */}
<div className="bg-yellow-50 p-2 rounded mt-1">
<SegmentMatchingRuleEditor
segmentId={segment.id}
currentRule={segment.matching_rule}
onRuleUpdated={(newRule) => {
// 可以在这里添加更新后的回调逻辑
console.log('片段匹配规则已更新:', segment.id, newRule);
}}
/>
</div>
{segment.template_material_id && (
<div>
使ID: <span className="font-mono text-blue-600">{segment.template_material_id}</span>

View File

@@ -7,7 +7,8 @@ import {
ImportTemplateRequest,
BatchImportRequest,
ImportProgress,
ImportStatus
ImportStatus,
SegmentMatchingRule
} from '../types/template';
interface TemplateQueryParams {
@@ -33,6 +34,8 @@ interface TemplateStore {
deleteTemplate: (id: string) => Promise<void>;
updateTemplate: (template: Template) => Promise<void>;
getImportProgress: (templateId: string) => Promise<ImportProgress | null>;
updateSegmentMatchingRule: (segmentId: string, matchingRule: SegmentMatchingRule) => Promise<void>;
getSegmentMatchingRule: (segmentId: string) => Promise<SegmentMatchingRule>;
clearError: () => void;
setCurrentTemplate: (template: Template | null) => void;
}
@@ -172,6 +175,42 @@ export const useTemplateStore = create<TemplateStore>((set, get) => ({
set({ error: null });
},
updateSegmentMatchingRule: async (segmentId: string, matchingRule: SegmentMatchingRule) => {
set({ error: null });
try {
await invoke('update_segment_matching_rule', {
segmentId,
matchingRule
});
// 如果当前有模板详情,刷新模板数据
const currentTemplate = get().currentTemplate;
if (currentTemplate) {
await get().getTemplateById(currentTemplate.id);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '更新片段匹配规则失败';
set({ error: errorMessage });
throw error;
}
},
getSegmentMatchingRule: async (segmentId: string) => {
set({ error: null });
try {
const matchingRule: SegmentMatchingRule = await invoke('get_segment_matching_rule', {
segmentId
});
return matchingRule;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '获取片段匹配规则失败';
set({ error: errorMessage });
throw error;
}
},
setCurrentTemplate: (template: Template | null) => {
set({ currentTemplate: template });
},

View File

@@ -52,6 +52,69 @@ export interface Track {
updated_at: string;
}
/**
* 片段匹配规则类型
* 匹配Rust enum的序列化格式
*/
export type SegmentMatchingRule =
| "FixedMaterial"
| { AiClassification: { category_id: string; category_name: string } };
/**
* 片段匹配规则辅助函数
*/
export const SegmentMatchingRuleHelper = {
/**
* 创建固定素材规则
*/
createFixedMaterial(): SegmentMatchingRule {
return "FixedMaterial";
},
/**
* 创建AI分类规则
*/
createAiClassification(categoryId: string, categoryName: string): SegmentMatchingRule {
return { AiClassification: { category_id: categoryId, category_name: categoryName } };
},
/**
* 获取规则的显示名称
*/
getDisplayName(rule: SegmentMatchingRule): string {
if (rule === "FixedMaterial") {
return '固定素材';
} else if (typeof rule === 'object' && 'AiClassification' in rule) {
return `AI分类: ${rule.AiClassification.category_name}`;
}
return '未知规则';
},
/**
* 检查是否为固定素材
*/
isFixedMaterial(rule: SegmentMatchingRule): boolean {
return rule === "FixedMaterial";
},
/**
* 检查是否为AI分类
*/
isAiClassification(rule: SegmentMatchingRule): boolean {
return typeof rule === 'object' && 'AiClassification' in rule;
},
/**
* 获取AI分类信息
*/
getAiClassificationInfo(rule: SegmentMatchingRule): { category_id: string; category_name: string } | null {
if (typeof rule === 'object' && 'AiClassification' in rule) {
return rule.AiClassification;
}
return null;
}
};
export interface TrackSegment {
id: string;
track_id: string;
@@ -62,6 +125,7 @@ export interface TrackSegment {
duration: number; // 片段时长(微秒)
segment_index: number; // 片段在轨道中的索引
properties?: string; // JSON格式的片段属性
matching_rule: SegmentMatchingRule; // 片段匹配规则
created_at: string;
updated_at: string;
}