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
24 changes: 11 additions & 13 deletions freevideo_engine/release_notes.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
{
"schema": 1,
"product_version": "0.2.0",
"product_version": "0.2.1",
"en": {
"summary": "Four quality levels, a new two-pass refinement and sharing exports.",
"summary": "A redesigned quality slider, result view and sharing card.",
"changes": [
"Choose Light, Medium, High or Max with the quality slider. Light uses two-pass acceleration; the higher levels sample 12, 16 or 20 steps.",
"Two-pass generation now refines with three independent steps after the first 8.",
"Setup installs every quality level by default. Existing installations download only what a generation needs, before it starts; download time is not counted as generation time.",
"Export a sharing image or video with generation statistics. The original video stays unchanged.",
"The launcher shows the FreeVideo wordmark."
"The quality slider is larger and easier to drag, with clearer motion between levels.",
"A finished video now has one action bar: Download video, Share, and a Report menu that holds both the diagnostic report and the generation record.",
"Generation times and memory peaks sit together in one card, in Create and in Creations.",
"Sharing images and videos are simpler: generation time, GPU and quality level."
]
},
"zh": {
"summary": "四档质量、全新二次采样与分享导出。",
"summary": "质量滑块、生成结果区和分享卡片重新设计。",
"changes": [
"质量滑块提供轻量、标准、精细、极致四档。轻量使用二次采样加速,其余三档采样 12、16、20 步。",
"二次采样改为一采 8 步后,独立二采 3 步。",
"安装时默认下载全部质量档位。已安装的版本只在生成前补装当次需要的文件,下载时间不计入生成耗时。",
"可导出带生成统计的分享图片或视频,原视频保持不变。",
"启动器左上角显示 FreeVideo 字标。"
"质量滑块更大、更好拖动,切换档位的动效更清晰。",
"视频生成后只有一排操作:下载视频、分享,以及一个报告菜单,诊断报告和生成记录都在里面。",
"耗时和显存、内存峰值合并为一张卡片,创作页和作品库一致。",
"分享图片和视频更简洁,只保留生成耗时、显卡和质量档位。"
]
}
}
2 changes: 1 addition & 1 deletion web/generation_progress.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 14 additions & 8 deletions web/generation_progress.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,18 +98,24 @@ export function createGenerationProgress(t, now = () => Date.now(), {compact = f
// only a new request clears it.
return reportId;
}
// Download the redacted diagnostic export; throws when it is unavailable.
async function downloadReport() {
const id = reportId;
if (!id) throw new Error('Report unavailable');
const path = `/freevideo/report/${id}`;
const response = await (api ? api.fetchApi(path) : fetch(path));
if (!response.ok) throw new Error('Report unavailable');
const blob = await response.blob(), url = URL.createObjectURL(blob);
const link = document.createElement('a'); link.href = url; link.download = 'video.debug.json';
document.body.append(link); link.click(); link.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
report.onclick = async () => {
if (!reportId || report.disabled) return;
const id = reportId;
report.disabled = true; report.textContent = t('Preparing report…', '正在整理报告…');
try {
const path = `/freevideo/report/${id}`;
const response = await (api ? api.fetchApi(path) : fetch(path));
if (!response.ok) throw new Error('Report unavailable');
const blob = await response.blob(), url = URL.createObjectURL(blob);
const link = document.createElement('a'); link.href = url; link.download = 'video.debug.json';
document.body.append(link); link.click(); link.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
await downloadReport();
if (id === reportId) report.textContent = reportLabel();
} catch {
if (id === reportId) report.textContent = t('Report unavailable · retry', '报告暂不可用 · 重试');
Expand Down Expand Up @@ -323,5 +329,5 @@ export function createGenerationProgress(t, now = () => Date.now(), {compact = f
if (timer !== null) clearInterval(timer); timer = null; state = {}; overall = {}; shownFraction = null;
stopCount(); countShown = null;
}
return {element, report, updateReport, update, hide, dispose: hide};
return {element, report, updateReport, downloadReport, hasReport: () => reportId !== null, update, hide, dispose: hide};
}
12 changes: 3 additions & 9 deletions web/library.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { api } from '../../scripts/api.js';
import { closeDialog } from './motion.js';
import { outputDownloadURL } from './output_download.js';
import { shareButton } from './share.js';
import { resultActions } from './result_actions.js';

const style = document.createElement('link');
style.rel = 'stylesheet'; style.href = new URL('./library.css', import.meta.url).href; document.head.append(style);
Expand Down Expand Up @@ -45,7 +44,7 @@ export function openLibrary(t) {
stats.setAttribute('aria-label', t('Generation statistics', '生成统计'));
const budget = el('div', '', 'fv-budget');
const links = el('div', null, 'fv-result-links');
detail.append(all, frame, caption, stats, budget, links);
detail.append(all, frame, caption, links, stats, budget);
body.append(collection, detail); dialog.append(header, body);
let disposed = false, loading = false, next = null, selected = null, player = null, arrivals = [];
const rows = new Map(), cards = new Map();
Expand Down Expand Up @@ -80,12 +79,7 @@ export function openLibrary(t) {
}
}, {once: true});
date.textContent = timestamp(row); geometry.textContent = dimensions(row);
links.replaceChildren();
for (const [label, file, cls] of [[t('Download video', '下载视频'), row.video, 'fv-primary'], [t('View report', '查看报告'), row.report, 'fv-quiet']]) {
if (!file) continue;
const a = el('a', label, cls); a.href = outputDownloadURL(api, file); a.download = file === row.video ? '' : file.split('/').pop(); links.append(a);
}
links.append(shareButton(row, t));
links.replaceChildren(resultActions(row, t));
for (const [file, card] of cards) card.setAttribute('aria-pressed', String(file === selected));
}
// These are the saved generation's measurements, including when the
Expand Down
23 changes: 23 additions & 0 deletions web/result_actions.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

98 changes: 98 additions & 0 deletions web/result_actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { api } from '../../scripts/api.js';
import { outputDownloadURL } from './output_download.js';
import { shareButton } from './share.js';

const css = document.createElement('link');
css.rel = 'stylesheet'; css.href = new URL('./result_actions.css', import.meta.url).href; document.head.append(css);

const ICONS = {
download: '<path d="M8 2.5v7.5m0 0 3-3m-3 3-3-3M3 11.5v1a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1"/>',
share: '<path d="M8 10V2.5m0 0L5.2 5.3M8 2.5l2.8 2.8M4.5 7.5H4a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-4a1 1 0 0 0-1-1h-.5"/>',
report: '<path d="M9.5 2H5a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.5L9.5 2Z"/><path d="M9.5 2v2.5H12M6.5 8h3M6.5 10.5h3"/>',
chevron: '<path d="m4.5 6.5 3.5 3 3.5-3"/>',
};

export function icon(name) {
const span = document.createElement('span'); span.className = 'fv-icon'; span.setAttribute('aria-hidden', 'true');
span.innerHTML = `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${ICONS[name]}</svg>`;
return span;
}

function action(tag, label, name, cls) {
const e = document.createElement(tag); e.className = 'fv-action ' + cls;
const text = document.createElement('span'); text.textContent = label;
e.append(icon(name), text);
if (tag === 'button') e.type = 'button';
return e;
}

// Download the video first; share and the reports follow, one entry each.
// `diagnostic` is an async callback for the redacted support report, when one exists.
export function resultActions(record, t, {diagnostic = null} = {}) {
const bar = document.createElement('div'); bar.className = 'fv-actions';
if (record.video) {
const download = action('a', t('Download video', '下载视频'), 'download', 'fv-action-primary');
download.href = outputDownloadURL(api, record.video); download.download = '';
bar.append(download);
}
const share = shareButton(record, t);
share.className = 'fv-action fv-action-secondary fv-share-trigger';
const label = document.createElement('span'); label.textContent = share.textContent;
share.replaceChildren(icon('share'), label);
bar.append(share);
const recordLink = record.report ? outputDownloadURL(api, record.report) : null;
if (!diagnostic) {
if (recordLink) {
const report = action('a', t('Report', '报告'), 'report', 'fv-action-secondary');
report.href = recordLink; report.download = record.report.split('/').pop(); report.title = t('Generation record (JSON)', '生成记录(JSON)');
bar.append(report);
}
return bar;
}
const menu = document.createElement('details'); menu.className = 'fv-action-menu';
const summary = document.createElement('summary'); summary.className = 'fv-action fv-action-secondary';
const text = document.createElement('span'); text.textContent = t('Report', '报告');
summary.append(icon('report'), text, icon('chevron'));
const list = document.createElement('div'); list.className = 'fv-menu';
const item = (tag, title, hint) => {
const e = document.createElement(tag); e.className = 'fv-menu-item';
const strong = document.createElement('strong'); strong.textContent = title;
const small = document.createElement('small'); small.textContent = hint;
e.append(strong, small);
return e;
};
const supportLabel = t('Diagnostic report', '诊断报告');
const support = item('button', supportLabel, t('Redacted, for problem reports', '已脱敏,可用于反馈问题')); support.type = 'button';
const supportTitle = support.querySelector('strong');
support.onclick = async () => {
if (support.disabled) return;
support.disabled = true; supportTitle.textContent = t('Preparing…', '正在整理…');
try { await diagnostic(); supportTitle.textContent = supportLabel; menu.open = false; }
catch { supportTitle.textContent = t('Report unavailable · retry', '报告暂不可用 · 重试'); }
finally { support.disabled = false; }
};
list.append(support);
if (recordLink) {
const full = item('a', t('Generation record', '生成记录'), t('Every setting of this run (JSON)', '本次生成的完整参数(JSON)'));
full.href = recordLink; full.download = record.report.split('/').pop();
full.onclick = () => { menu.open = false; };
list.append(full);
}
menu.append(summary, list);
// Close on an outside click or Escape, like a native menu. Escape must not
// also reach the dialog, which would close the whole window.
const outside = event => { if (menu.open && !menu.contains(event.target)) menu.open = false; };
menu.addEventListener('toggle', () => {
if (menu.open) document.addEventListener('pointerdown', outside, true);
else {
document.removeEventListener('pointerdown', outside, true);
if (!support.disabled) supportTitle.textContent = supportLabel;
}
});
menu.addEventListener('keydown', event => {
if (event.key !== 'Escape' || !menu.open) return;
event.preventDefault(); event.stopPropagation(); menu.open = false; summary.focus();
});
bar.append(menu);
return bar;
}
Loading