* feat(editor): 剪辑成片验收闭环 - 概览渲染成功计数、媒体导入/字幕/预览修复与本地 whisper 转写运行时 - 概览:渲染成功计数接入项目概览 SQL/JSON,成片状态随 timeline_render 任务联动 - 修复:本地媒体导入后列表为空(挂载时服务端重同步 + 导入后校验重取)、 字幕工具残留 M6 占位文案、预览监视器悬浮遮挡层改为顶部信息条、 生成任务轮询连续查询失败即抛错不再空转至超时 - 本地转写:新增 scripts/start-whisper-local.sh 一键启动 whisper.cpp, local-development 文档补安装/启动/后端注入指引 - 同步 docs(overview/features、pending-test、index) * fix(assets): 素材库封面未知 kind 兜底,避免 AssetCover 渲染崩溃 运行时素材记录若带未知/脏 kind(历史遗留或本地存储异常),assetKindIcons[asset.kind] 得到 undefined,AssetCover/AssetDrawer 渲染 <KindIcon/> 触发 'Element type is invalid ... got: undefined' 使 /assets 整页崩溃。 新增 isKnownAssetKind 守卫:未知 kind 回退 FileText 图标与通用文案,脏数据不再崩页。 * fix(editor): mp4v 播放死循环修复——MPEG-4 Part 2 归入播放转码通道,监视器加终态护栏 - 后端把 mp4v 与 H.264/AV1/VP9 一并判为浏览器可播 → playbackStatus=none; Chromium/Firefox/Edge 实际均不支持 MPEG-4 Part 2,原件解码即失败。 现将其并入 H.265 转码通道(转 H.264 兼容副本);损坏文件转码失败落 failed,前端据此显示终止性错误而非死循环。 - 前端监视器轮询到 none/failed 或超时后不再回退 primary 重试;副本 URL 回退原件反复触发 onError 时保持终态文案,不再覆盖成"正在生成兼容版本"。 * feat(editor): 画布产物自动同步剪辑素材库,素材面板新增「画布素材」分组 - 打开剪辑器时自动把画布工作台当前采用(storyboard/action_board/分镜/导出成片, ready 且有资源)的产物 linkProjectAsset(source=canvas) 并入项目素材库; 后端幂等、前端按 storageKey+会话 ref 去重,失败静默下次进入自动补同步。 - LinkProjectAsset 支持 source 白名单(uploaded/canvas)并写入素材 payload, 供素材面板按来源分组;面板新增「画布素材」页签与分组,与「本地上传」「项目素材」并列。 - 补 source=canvas 写入断言测试。 * docs(progress): 记录 mp4v 播放死循环修复与画布产物素材同步的待浏览器验收项
245 lines
12 KiB
Go
245 lines
12 KiB
Go
package repository
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"infinite-canvas/backend/internal/model"
|
|
)
|
|
|
|
type ProjectOverviewMetrics struct {
|
|
UnitCount int64 `gorm:"column:unit_count"`
|
|
CompletedUnitCount int64 `gorm:"column:completed_unit_count"`
|
|
TotalWordCount int64 `gorm:"column:total_word_count"`
|
|
UnitsWithoutText int64 `gorm:"column:units_without_text"`
|
|
UnitsWithoutShots int64 `gorm:"column:units_without_shots"`
|
|
CanvasCount int64 `gorm:"column:canvas_count"`
|
|
AssetCount int64 `gorm:"column:asset_count"`
|
|
ShotCount int64 `gorm:"column:shot_count"`
|
|
PendingCandidateCount int64 `gorm:"column:pending_candidate_count"`
|
|
ReadyStoryboardCount int64 `gorm:"column:ready_storyboard_count"`
|
|
ReadyPrevizCount int64 `gorm:"column:ready_previz_count"`
|
|
ReadyVideoCount int64 `gorm:"column:ready_video_count"`
|
|
TimelineRenderSucceededCount int64 `gorm:"column:timeline_render_succeeded_count"`
|
|
StaleArtifactCount int64 `gorm:"column:stale_artifact_count"`
|
|
}
|
|
|
|
type ProjectOverviewUnitRow struct {
|
|
model.ProjectUnit
|
|
ShotCount int64 `gorm:"column:shot_count"`
|
|
CandidateCount int64 `gorm:"column:candidate_count"`
|
|
CanvasCount int64 `gorm:"column:canvas_count"`
|
|
}
|
|
|
|
type ProjectAssetCountRow struct {
|
|
Key string `gorm:"column:key"`
|
|
Count int64 `gorm:"column:count"`
|
|
}
|
|
|
|
func (r *Repository) ProjectUnitCanvasCounts(projectID string) (map[string]int64, error) {
|
|
var rows []ProjectAssetCountRow
|
|
if err := r.db.Table("canvas_unit_links").Select("unit_id AS key, COUNT(DISTINCT canvas_id) AS count").Where("project_id = ?", projectID).Group("unit_id").Scan(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
counts := make(map[string]int64, len(rows))
|
|
for _, row := range rows {
|
|
counts[row.Key] = row.Count
|
|
}
|
|
return counts, nil
|
|
}
|
|
|
|
func (r *Repository) ProjectOverviewMetrics(projectID string) (ProjectOverviewMetrics, error) {
|
|
var metrics ProjectOverviewMetrics
|
|
err := r.db.Raw(`
|
|
SELECT
|
|
(SELECT COUNT(*) FROM project_units WHERE project_id = ?) AS unit_count,
|
|
(SELECT COUNT(*) FROM project_units WHERE project_id = ? AND status = ?) AS completed_unit_count,
|
|
(SELECT COALESCE(SUM(word_count), 0) FROM project_units WHERE project_id = ?) AS total_word_count,
|
|
(SELECT COUNT(*) FROM project_units WHERE project_id = ? AND word_count = 0) AS units_without_text,
|
|
(SELECT COUNT(*) FROM project_units pu WHERE pu.project_id = ? AND pu.status <> ? AND NOT EXISTS (SELECT 1 FROM shots s WHERE s.project_id = pu.project_id AND s.unit_id = pu.id)) AS units_without_shots,
|
|
(SELECT COUNT(*) FROM canvas_projects WHERE project_id = ?) AS canvas_count,
|
|
(SELECT COUNT(*) FROM project_asset_links WHERE project_id = ?) AS asset_count,
|
|
(SELECT COUNT(*) FROM shots WHERE project_id = ?) AS shot_count,
|
|
(SELECT COUNT(*) FROM project_asset_candidates WHERE project_id = ? AND status = 'pending_confirmation') AS pending_candidate_count,
|
|
(SELECT COUNT(DISTINCT shot_id) FROM shot_artifacts WHERE project_id = ? AND type = 'storyboard' AND selected = ? AND status = 'ready') AS ready_storyboard_count,
|
|
(SELECT COUNT(DISTINCT shot_id) FROM shot_artifacts WHERE project_id = ? AND type = 'action_board' AND selected = ? AND status = 'ready') AS ready_previz_count,
|
|
(SELECT COUNT(DISTINCT shot_id) FROM shot_artifacts WHERE project_id = ? AND type = 'video' AND selected = ? AND status = 'ready') AS ready_video_count,
|
|
(SELECT COUNT(*) FROM tasks WHERE project_id = ? AND type = ? AND status = ?) AS timeline_render_succeeded_count,
|
|
(SELECT COUNT(*) FROM shot_artifacts WHERE project_id = ? AND status = 'stale') AS stale_artifact_count
|
|
`, projectID, projectID, model.ProjectUnitStatusCompleted, projectID, projectID, projectID, model.ProjectUnitStatusDraft, projectID, projectID, projectID, projectID, projectID, true, projectID, true, projectID, true, projectID, model.TaskTypeTimelineRender, model.TaskStatusSucceeded, projectID).Scan(&metrics).Error
|
|
return metrics, err
|
|
}
|
|
|
|
func (r *Repository) ProjectOverviewUnits(projectID string, limit int) ([]ProjectOverviewUnitRow, error) {
|
|
if limit <= 0 || limit > 20 {
|
|
limit = 8
|
|
}
|
|
var rows []ProjectOverviewUnitRow
|
|
err := r.db.Raw(`
|
|
SELECT pu.id, pu.project_id, pu.parent_id, pu.kind, pu.title, pu.word_count, pu.status, pu.position, pu.created_at, pu.updated_at,
|
|
(SELECT COUNT(*) FROM shots s WHERE s.project_id = pu.project_id AND s.unit_id = pu.id) AS shot_count,
|
|
(SELECT COUNT(*) FROM project_asset_candidates pac WHERE pac.project_id = pu.project_id AND pac.unit_id = pu.id) AS candidate_count,
|
|
(SELECT COUNT(DISTINCT cul.canvas_id) FROM canvas_unit_links cul WHERE cul.project_id = pu.project_id AND cul.unit_id = pu.id) AS canvas_count
|
|
FROM project_units pu
|
|
WHERE pu.project_id = ?
|
|
ORDER BY pu.position ASC, pu.created_at ASC
|
|
LIMIT ?
|
|
`, projectID, limit).Scan(&rows).Error
|
|
return rows, err
|
|
}
|
|
|
|
func (r *Repository) ProjectUnitShots(projectID string, unitID string) ([]model.Shot, error) {
|
|
var shots []model.Shot
|
|
err := r.db.Where("project_id = ? AND unit_id = ?", projectID, unitID).Order("position asc, created_at asc").Find(&shots).Error
|
|
return shots, err
|
|
}
|
|
|
|
func (r *Repository) ProjectUnitShotRevisions(projectID string, unitID string) ([]model.ShotRevision, error) {
|
|
var revisions []model.ShotRevision
|
|
err := r.db.Table("shot_revisions").Select("shot_revisions.*").Joins("JOIN shots ON shots.id = shot_revisions.shot_id").Where("shots.project_id = ? AND shots.unit_id = ?", projectID, unitID).Order("shots.position asc, shot_revisions.version asc").Scan(&revisions).Error
|
|
return revisions, err
|
|
}
|
|
|
|
func (r *Repository) ProjectUnitShotArtifacts(projectID string, unitID string) ([]model.ShotArtifact, error) {
|
|
var artifacts []model.ShotArtifact
|
|
err := r.db.Where("project_id = ? AND unit_id = ?", projectID, unitID).Order("shot_id asc, type asc, version asc").Find(&artifacts).Error
|
|
return artifacts, err
|
|
}
|
|
|
|
func (r *Repository) ProjectUnitShotAssetReferences(projectID string, unitID string) ([]model.ShotAssetReference, error) {
|
|
var references []model.ShotAssetReference
|
|
err := r.db.Table("shot_asset_references").Select("shot_asset_references.*").Joins("JOIN shots ON shots.id = shot_asset_references.shot_id").Where("shots.project_id = ? AND shots.unit_id = ?", projectID, unitID).Order("shot_asset_references.created_at asc").Scan(&references).Error
|
|
return references, err
|
|
}
|
|
|
|
func (r *Repository) ProjectAssetVersionsByIDs(projectID string, versionIDs []string) ([]model.AssetVersion, error) {
|
|
if len(versionIDs) == 0 {
|
|
return []model.AssetVersion{}, nil
|
|
}
|
|
var versions []model.AssetVersion
|
|
err := r.db.Table("asset_versions").Select("asset_versions.*").
|
|
Joins("JOIN project_asset_links ON project_asset_links.asset_id = asset_versions.asset_id").
|
|
Where("project_asset_links.project_id = ? AND asset_versions.id IN ?", projectID, versionIDs).
|
|
Find(&versions).Error
|
|
return versions, err
|
|
}
|
|
|
|
func (r *Repository) AssetRepresentationsByVersionIDs(versionIDs []string) ([]model.AssetRepresentation, error) {
|
|
if len(versionIDs) == 0 {
|
|
return []model.AssetRepresentation{}, nil
|
|
}
|
|
var representations []model.AssetRepresentation
|
|
err := r.db.Where("asset_version_id IN ?", versionIDs).Order("asset_version_id asc, role asc, created_at asc").Find(&representations).Error
|
|
return representations, err
|
|
}
|
|
|
|
func (r *Repository) ProjectUnitAssetCandidates(projectID string, unitID string) ([]model.ProjectAssetCandidate, error) {
|
|
var candidates []model.ProjectAssetCandidate
|
|
err := r.db.Where("project_id = ? AND (unit_id = ? OR unit_id = '')", projectID, unitID).Order("created_at asc").Find(&candidates).Error
|
|
return candidates, err
|
|
}
|
|
|
|
func (r *Repository) ProjectUnitAssets(userID string, projectID string, unitID string) ([]model.Asset, error) {
|
|
var assets []model.Asset
|
|
err := r.db.Table("assets").Distinct("assets.*").
|
|
Joins("JOIN project_asset_links pal ON pal.asset_id = assets.id AND pal.project_id = ?", projectID).
|
|
Joins("JOIN asset_versions av ON av.asset_id = assets.id").
|
|
Joins("JOIN shot_asset_references sar ON sar.asset_version_id = av.id").
|
|
Joins("JOIN shots s ON s.id = sar.shot_id AND s.project_id = ? AND s.unit_id = ?", projectID, unitID).
|
|
Where("assets.user_id = ?", userID).
|
|
Order("assets.updated_at desc").
|
|
Scan(&assets).Error
|
|
return assets, err
|
|
}
|
|
|
|
func (r *Repository) ProjectWorkflowInstancesForUnit(projectID string, unitID string) ([]model.WorkflowInstance, error) {
|
|
var instances []model.WorkflowInstance
|
|
err := r.db.Where("project_id = ? AND unit_id = ?", projectID, unitID).Order("created_at asc").Find(&instances).Error
|
|
return instances, err
|
|
}
|
|
|
|
func (r *Repository) ProjectCanvasSummariesPage(userID string, projectID string, page int, pageSize int) ([]model.CanvasProject, int64, error) {
|
|
var canvases []model.CanvasProject
|
|
var total int64
|
|
query := r.db.Model(&model.CanvasProject{}).Where("user_id = ? AND project_id = ?", userID, projectID)
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
err := query.Select("id", "user_id", "project_id", "title", "created_at", "updated_at").Order("updated_at desc").Offset((page - 1) * pageSize).Limit(pageSize).Find(&canvases).Error
|
|
return canvases, total, err
|
|
}
|
|
|
|
func (r *Repository) ProjectCanvasUnitLinksForCanvases(projectID string, canvasIDs []string) ([]model.CanvasUnitLink, error) {
|
|
if len(canvasIDs) == 0 {
|
|
return []model.CanvasUnitLink{}, nil
|
|
}
|
|
var links []model.CanvasUnitLink
|
|
err := r.db.Where("project_id = ? AND canvas_id IN ?", projectID, canvasIDs).Order("created_at asc").Find(&links).Error
|
|
return links, err
|
|
}
|
|
|
|
func (r *Repository) ProjectAssetCandidatesPage(projectID string, page int, pageSize int, unitID string, status string, category string, queryText string) ([]model.ProjectAssetCandidate, int64, error) {
|
|
var candidates []model.ProjectAssetCandidate
|
|
var total int64
|
|
query := r.db.Model(&model.ProjectAssetCandidate{}).Where("project_id = ?", projectID)
|
|
if value := strings.TrimSpace(unitID); value != "" {
|
|
query = query.Where("unit_id = ?", value)
|
|
}
|
|
if value := strings.TrimSpace(status); value != "" {
|
|
query = query.Where("status = ?", value)
|
|
}
|
|
if value := strings.TrimSpace(category); value != "" {
|
|
query = query.Where("category = ?", value)
|
|
}
|
|
if value := strings.TrimSpace(queryText); value != "" {
|
|
query = query.Where("LOWER(name) LIKE ?", "%"+strings.ToLower(value)+"%")
|
|
}
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
err := query.Order("created_at desc").Offset((page - 1) * pageSize).Limit(pageSize).Find(&candidates).Error
|
|
return candidates, total, err
|
|
}
|
|
|
|
func (r *Repository) ProjectAssetsPage(userID string, projectID string, page int, pageSize int, category string, mediaType string, status string, folderID *string, queryText string) ([]model.Asset, int64, error) {
|
|
var assets []model.Asset
|
|
var total int64
|
|
query := r.db.Table("assets").Joins("JOIN project_asset_links ON project_asset_links.asset_id = assets.id").Where("assets.user_id = ? AND project_asset_links.project_id = ?", userID, projectID)
|
|
if value := strings.TrimSpace(category); value != "" {
|
|
query = query.Where("assets.category = ?", value)
|
|
}
|
|
if value := strings.TrimSpace(mediaType); value != "" {
|
|
query = query.Where("assets.kind = ?", value)
|
|
}
|
|
if value := strings.TrimSpace(status); value != "" {
|
|
query = query.Where("assets.status = ?", value)
|
|
}
|
|
if folderID != nil {
|
|
query = query.Where("project_asset_links.folder_id = ?", strings.TrimSpace(*folderID))
|
|
}
|
|
if value := strings.TrimSpace(queryText); value != "" {
|
|
query = query.Where("LOWER(assets.title) LIKE ?", "%"+strings.ToLower(value)+"%")
|
|
}
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
err := query.Select("assets.*").Order("assets.updated_at desc").Offset((page - 1) * pageSize).Limit(pageSize).Scan(&assets).Error
|
|
return assets, total, err
|
|
}
|
|
|
|
func (r *Repository) ProjectAssetFacets(projectID string) ([]ProjectAssetCountRow, []ProjectAssetCountRow, error) {
|
|
var categoryRows []ProjectAssetCountRow
|
|
if err := r.db.Table("assets").Select("assets.category AS key, COUNT(*) AS count").
|
|
Joins("JOIN project_asset_links pal ON pal.asset_id = assets.id").
|
|
Where("pal.project_id = ?", projectID).
|
|
Group("assets.category").Scan(&categoryRows).Error; err != nil {
|
|
return nil, nil, err
|
|
}
|
|
var folderRows []ProjectAssetCountRow
|
|
if err := r.db.Table("project_asset_links").Select("folder_id AS key, COUNT(*) AS count").
|
|
Where("project_id = ?", projectID).
|
|
Group("folder_id").Scan(&folderRows).Error; err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return categoryRows, folderRows, nil
|
|
}
|