Spaces:
Running
Running
fix: Seedance content-policy survival \u2014 anonymized image prompts + auto-fallback chain + per-anchor retry UI
Browse files- aether_ad/media/fal_client.py +10 -3
- static/app.js +105 -40
aether_ad/media/fal_client.py
CHANGED
|
@@ -30,10 +30,17 @@ _IMAGE_QUALITY_SUFFIX = (
|
|
| 30 |
"Style: hyperrealistic cinematography, IMAX-grade composition, ARRI Alexa "
|
| 31 |
"cinema look, anamorphic depth of field, professional color grading, 8K "
|
| 32 |
"detail, editorial magazine framing, golden-hour or rim lighting, motivated "
|
| 33 |
-
"natural lighting only.
|
| 34 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
"Avoid: blurry, low-resolution, amateur snapshot, watermark, plastic skin, "
|
| 36 |
-
"morphing artifacts, deformed hands, extra fingers, wrong anatomy
|
|
|
|
| 37 |
"Strictly NO text, NO letters, NO Korean characters, NO captions, NO subtitles, "
|
| 38 |
"NO writing, NO typography, NO logos anywhere in the image. Pure visual scene only."
|
| 39 |
)
|
|
|
|
| 30 |
"Style: hyperrealistic cinematography, IMAX-grade composition, ARRI Alexa "
|
| 31 |
"cinema look, anamorphic depth of field, professional color grading, 8K "
|
| 32 |
"detail, editorial magazine framing, golden-hour or rim lighting, motivated "
|
| 33 |
+
"natural lighting only. Surfaces have authentic texture (no plastic or waxy "
|
| 34 |
+
"look). Subjects are sharply in focus, background has cinematic bokeh. "
|
| 35 |
+
# โโ Anonymization (Seedance 2.0 i2v rejects recognizable real-person likenesses)
|
| 36 |
+
"If human subjects appear, render them anonymized: prefer back-view, "
|
| 37 |
+
"side-profile, silhouette, half-shadow, hair-obscured, hands-only, or "
|
| 38 |
+
"subject framed from behind. Avoid clear frontal close-up portraits that "
|
| 39 |
+
"could resemble identifiable real people. Stylized, anonymous, atmospheric. "
|
| 40 |
+
# โโ General negatives
|
| 41 |
"Avoid: blurry, low-resolution, amateur snapshot, watermark, plastic skin, "
|
| 42 |
+
"morphing artifacts, deformed hands, extra fingers, wrong anatomy, frontal "
|
| 43 |
+
"celebrity-like portraits. "
|
| 44 |
"Strictly NO text, NO letters, NO Korean characters, NO captions, NO subtitles, "
|
| 45 |
"NO writing, NO typography, NO logos anywhere in the image. Pure visual scene only."
|
| 46 |
)
|
static/app.js
CHANGED
|
@@ -662,58 +662,105 @@ function buildCombinedScenePrompt(beats) {
|
|
| 662 |
return lines.join("\n");
|
| 663 |
}
|
| 664 |
|
| 665 |
-
|
| 666 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 667 |
const seed = LATEST_RESULTS?.seeds?.[sIdx]?.seed;
|
| 668 |
if (!seed) return;
|
| 669 |
const beats = seed.beats || [];
|
| 670 |
if (!beats.length) return;
|
| 671 |
|
| 672 |
-
//
|
| 673 |
-
const
|
| 674 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 675 |
FINAL_VIDEO_STATE[sIdx] = {
|
| 676 |
status: 'error',
|
| 677 |
-
message: '์ด๋ฏธ์ง๊ฐ ์์ง ์์ฑ๋์ง ์์ ๋น๋์ค
|
| 678 |
};
|
| 679 |
renderFinalVideoSection(sIdx);
|
| 680 |
return;
|
| 681 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 682 |
|
| 683 |
const totalSec = beats[beats.length - 1].time_range[1];
|
| 684 |
const combinedScene = buildCombinedScenePrompt(beats);
|
| 685 |
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
};
|
| 691 |
-
renderFinalVideoSection(sIdx);
|
| 692 |
-
|
| 693 |
-
try {
|
| 694 |
-
const r = await fetch('/api/media/video', {
|
| 695 |
-
method: 'POST',
|
| 696 |
-
headers: {'Content-Type': 'application/json'},
|
| 697 |
-
body: JSON.stringify({
|
| 698 |
-
scene: combinedScene,
|
| 699 |
-
image_url: anchorImg,
|
| 700 |
-
duration_seconds: totalSec,
|
| 701 |
-
}),
|
| 702 |
-
});
|
| 703 |
-
const data = await r.json();
|
| 704 |
-
if (!r.ok) throw new Error(data.detail || `HTTP ${r.status}`);
|
| 705 |
-
FINAL_VIDEO_STATE[sIdx] = {
|
| 706 |
-
status: 'ready',
|
| 707 |
-
video_url: data.video_url,
|
| 708 |
-
target_seconds: totalSec,
|
| 709 |
-
};
|
| 710 |
-
} catch (e) {
|
| 711 |
FINAL_VIDEO_STATE[sIdx] = {
|
| 712 |
-
status: '
|
| 713 |
target_seconds: totalSec,
|
| 714 |
-
|
|
|
|
|
|
|
|
|
|
| 715 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 716 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 717 |
renderFinalVideoSection(sIdx);
|
| 718 |
}
|
| 719 |
|
|
@@ -739,12 +786,16 @@ function renderFinalVideoSection(sIdx) {
|
|
| 739 |
}
|
| 740 |
|
| 741 |
if (st.video_url) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 742 |
html.push(`
|
| 743 |
<div class="media-thumb stitch-thumb">
|
| 744 |
<video src="${escapeAttr(st.video_url)}" controls playsinline preload="metadata"></video>
|
| 745 |
</div>
|
| 746 |
<div class="stitch-progress">
|
| 747 |
-
${totalSec}์ด ์์ ยท
|
| 748 |
</div>
|
| 749 |
<div class="stitch-actions">
|
| 750 |
<a class="btn-stitch-download"
|
|
@@ -756,6 +807,13 @@ function renderFinalVideoSection(sIdx) {
|
|
| 756 |
} else if (st.status !== 'loading') {
|
| 757 |
const enough = imagesReady >= 1;
|
| 758 |
const disabled = !enough ? 'disabled' : '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 759 |
html.push(`
|
| 760 |
<div class="stitch-progress">
|
| 761 |
์ด๋ฏธ์ง ${imagesReady}/${beats.length} ์ค๋น
|
|
@@ -764,6 +822,7 @@ function renderFinalVideoSection(sIdx) {
|
|
| 764 |
<button type="button" class="btn-stitch" data-final-seed="${sIdx}" ${disabled}>
|
| 765 |
๐ฌ ${totalSec}์ด ๊ด๊ณ ์์ ์์ฑ (Seedance 2.0, ํ ๋ฒ ํธ์ถ)
|
| 766 |
</button>
|
|
|
|
| 767 |
`);
|
| 768 |
}
|
| 769 |
|
|
@@ -952,14 +1011,21 @@ function escapeAttr(s) {
|
|
| 952 |
return String(s || '').replace(/"/g, '"').replace(/</g, '<');
|
| 953 |
}
|
| 954 |
|
| 955 |
-
/* Delegate clicks on per-beat image buttons + final-video regen
|
| 956 |
document.addEventListener('click', (e) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 957 |
const finalBtn = e.target.closest('.btn-stitch');
|
| 958 |
if (finalBtn && !finalBtn.disabled) {
|
| 959 |
-
const sIdx = parseInt(
|
| 960 |
-
finalBtn.dataset.finalSeed ?? finalBtn.dataset.stitchSeed,
|
| 961 |
-
10,
|
| 962 |
-
);
|
| 963 |
if (!Number.isNaN(sIdx)) generateFinalVideo(sIdx);
|
| 964 |
return;
|
| 965 |
}
|
|
@@ -970,7 +1036,6 @@ document.addEventListener('click', (e) => {
|
|
| 970 |
if (btn.dataset.mediaBtn === 'image') {
|
| 971 |
manualGenerateImage(sIdx, bIdx);
|
| 972 |
}
|
| 973 |
-
// Per-beat video buttons removed โ single Seedance video at the bottom only.
|
| 974 |
});
|
| 975 |
|
| 976 |
|
|
|
|
| 662 |
return lines.join("\n");
|
| 663 |
}
|
| 664 |
|
| 665 |
+
function _isContentPolicyError(msg) {
|
| 666 |
+
const m = String(msg || '').toLowerCase();
|
| 667 |
+
return m.includes('content_policy_violation')
|
| 668 |
+
|| m.includes('content policy')
|
| 669 |
+
|| m.includes('likenesses of real people')
|
| 670 |
+
|| m.includes('private information');
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
/** Generate a single full-spot video (Seedance 2.0).
|
| 674 |
+
* Anchor = first chained image. If Seedance rejects on content policy,
|
| 675 |
+
* automatically retry with the next image in the chain. */
|
| 676 |
+
async function generateFinalVideo(sIdx, anchorBeatIdx = 0) {
|
| 677 |
const seed = LATEST_RESULTS?.seeds?.[sIdx]?.seed;
|
| 678 |
if (!seed) return;
|
| 679 |
const beats = seed.beats || [];
|
| 680 |
if (!beats.length) return;
|
| 681 |
|
| 682 |
+
// Build candidate list (any beat with an image) starting from anchorBeatIdx
|
| 683 |
+
const allCandidates = [];
|
| 684 |
+
for (let i = 0; i < beats.length; i++) {
|
| 685 |
+
const u = MEDIA_STATE[sIdx]?.[i]?.image_url;
|
| 686 |
+
if (u) allCandidates.push({ beatIdx: i, url: u });
|
| 687 |
+
}
|
| 688 |
+
if (!allCandidates.length) {
|
| 689 |
FINAL_VIDEO_STATE[sIdx] = {
|
| 690 |
status: 'error',
|
| 691 |
+
message: '์ด๋ฏธ์ง๊ฐ ์์ง ์์ฑ๋์ง ์์ ๋น๋์ค๋ฅผ ์์ํ ์ ์์ต๋๋ค.',
|
| 692 |
};
|
| 693 |
renderFinalVideoSection(sIdx);
|
| 694 |
return;
|
| 695 |
}
|
| 696 |
+
// Reorder so anchorBeatIdx comes first, then the rest in beat order
|
| 697 |
+
const candidates = [
|
| 698 |
+
...allCandidates.filter(c => c.beatIdx === anchorBeatIdx),
|
| 699 |
+
...allCandidates.filter(c => c.beatIdx !== anchorBeatIdx),
|
| 700 |
+
];
|
| 701 |
|
| 702 |
const totalSec = beats[beats.length - 1].time_range[1];
|
| 703 |
const combinedScene = buildCombinedScenePrompt(beats);
|
| 704 |
|
| 705 |
+
let lastErr = '';
|
| 706 |
+
let policyRejections = 0;
|
| 707 |
+
for (let attempt = 0; attempt < candidates.length; attempt++) {
|
| 708 |
+
const cand = candidates[attempt];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
FINAL_VIDEO_STATE[sIdx] = {
|
| 710 |
+
status: 'loading',
|
| 711 |
target_seconds: totalSec,
|
| 712 |
+
anchor_beat: cand.beatIdx,
|
| 713 |
+
message: attempt === 0
|
| 714 |
+
? `๐ฌ Seedance 2.0์ด ${totalSec}์ด ์์ ์์ฑ ์คโฆ (์ต์ปค: ๋นํธ ${cand.beatIdx + 1}, 3~6๋ถ)`
|
| 715 |
+
: `โ ๏ธ ์ ์ฑ
๊ฑฐ๋ถ ${policyRejections}ํ โ ๋นํธ ${cand.beatIdx + 1} ์ด๋ฏธ์ง๋ก ์ฌ์๋ ์คโฆ`,
|
| 716 |
};
|
| 717 |
+
renderFinalVideoSection(sIdx);
|
| 718 |
+
|
| 719 |
+
try {
|
| 720 |
+
const r = await fetch('/api/media/video', {
|
| 721 |
+
method: 'POST',
|
| 722 |
+
headers: {'Content-Type': 'application/json'},
|
| 723 |
+
body: JSON.stringify({
|
| 724 |
+
scene: combinedScene,
|
| 725 |
+
image_url: cand.url,
|
| 726 |
+
duration_seconds: totalSec,
|
| 727 |
+
}),
|
| 728 |
+
});
|
| 729 |
+
const data = await r.json();
|
| 730 |
+
if (!r.ok) throw new Error(data.detail || `HTTP ${r.status}`);
|
| 731 |
+
FINAL_VIDEO_STATE[sIdx] = {
|
| 732 |
+
status: 'ready',
|
| 733 |
+
video_url: data.video_url,
|
| 734 |
+
target_seconds: totalSec,
|
| 735 |
+
anchor_beat: cand.beatIdx,
|
| 736 |
+
policy_retries: policyRejections,
|
| 737 |
+
};
|
| 738 |
+
renderFinalVideoSection(sIdx);
|
| 739 |
+
return;
|
| 740 |
+
} catch (e) {
|
| 741 |
+
const msg = e.message || String(e);
|
| 742 |
+
lastErr = msg;
|
| 743 |
+
if (_isContentPolicyError(msg)) {
|
| 744 |
+
policyRejections += 1;
|
| 745 |
+
continue; // try next candidate
|
| 746 |
+
}
|
| 747 |
+
// non-policy error: stop, show
|
| 748 |
+
FINAL_VIDEO_STATE[sIdx] = {
|
| 749 |
+
status: 'error',
|
| 750 |
+
target_seconds: totalSec,
|
| 751 |
+
message: `๋น๋์ค ์์ฑ ์คํจ: ${msg}`,
|
| 752 |
+
};
|
| 753 |
+
renderFinalVideoSection(sIdx);
|
| 754 |
+
return;
|
| 755 |
+
}
|
| 756 |
}
|
| 757 |
+
// All candidates rejected by policy
|
| 758 |
+
FINAL_VIDEO_STATE[sIdx] = {
|
| 759 |
+
status: 'error',
|
| 760 |
+
target_seconds: totalSec,
|
| 761 |
+
message: `Seedance๊ฐ ๋ชจ๋ ์ด๋ฏธ์ง๋ฅผ ์ ์ฑ
์ผ๋ก ๊ฑฐ๋ถ (${policyRejections}ํ). `
|
| 762 |
+
+ `์ด๋ฏธ์ง๋ฅผ ์ฌ์์ฑํ ๋ค (์ธ๋ฌผ ์ ๋ฉด ํฌํธ๋ ์ดํธ ํํผ) ๋ค์ ์๋ํ์ธ์. ${lastErr.slice(0, 200)}`,
|
| 763 |
+
};
|
| 764 |
renderFinalVideoSection(sIdx);
|
| 765 |
}
|
| 766 |
|
|
|
|
| 786 |
}
|
| 787 |
|
| 788 |
if (st.video_url) {
|
| 789 |
+
const anchorIdx = (typeof st.anchor_beat === 'number') ? st.anchor_beat + 1 : 1;
|
| 790 |
+
const retryNote = st.policy_retries
|
| 791 |
+
? ` ยท ์ ์ฑ
๊ฑฐ๋ถ ${st.policy_retries}ํ ํ ์ฌ์๋ ์ฑ๊ณต`
|
| 792 |
+
: '';
|
| 793 |
html.push(`
|
| 794 |
<div class="media-thumb stitch-thumb">
|
| 795 |
<video src="${escapeAttr(st.video_url)}" controls playsinline preload="metadata"></video>
|
| 796 |
</div>
|
| 797 |
<div class="stitch-progress">
|
| 798 |
+
${totalSec}์ด ์์ ยท ${beats.length}๊ฐ ์ฌ ํตํฉ i2v ยท ์ต์ปค ์ด๋ฏธ์ง: ๋นํธ ${anchorIdx}${retryNote}
|
| 799 |
</div>
|
| 800 |
<div class="stitch-actions">
|
| 801 |
<a class="btn-stitch-download"
|
|
|
|
| 807 |
} else if (st.status !== 'loading') {
|
| 808 |
const enough = imagesReady >= 1;
|
| 809 |
const disabled = !enough ? 'disabled' : '';
|
| 810 |
+
// If a previous run errored on policy, surface a "Try with another anchor" hint
|
| 811 |
+
const anchorButtons = (st.status === 'error' && imagesReady > 1)
|
| 812 |
+
? beats.map((_, i) => MEDIA_STATE[sIdx]?.[i]?.image_url
|
| 813 |
+
? `<button type="button" class="btn-media" data-final-seed="${sIdx}" data-anchor-beat="${i}">๐ฏ ๋นํธ ${i + 1} ์ด๋ฏธ์ง๋ก ์ฌ์๋</button>`
|
| 814 |
+
: ''
|
| 815 |
+
).join('')
|
| 816 |
+
: '';
|
| 817 |
html.push(`
|
| 818 |
<div class="stitch-progress">
|
| 819 |
์ด๋ฏธ์ง ${imagesReady}/${beats.length} ์ค๋น
|
|
|
|
| 822 |
<button type="button" class="btn-stitch" data-final-seed="${sIdx}" ${disabled}>
|
| 823 |
๐ฌ ${totalSec}์ด ๊ด๊ณ ์์ ์์ฑ (Seedance 2.0, ํ ๋ฒ ํธ์ถ)
|
| 824 |
</button>
|
| 825 |
+
${anchorButtons ? `<div class="media-controls" style="flex-direction:row;flex-wrap:wrap;">${anchorButtons}</div>` : ''}
|
| 826 |
`);
|
| 827 |
}
|
| 828 |
|
|
|
|
| 1011 |
return String(s || '').replace(/"/g, '"').replace(/</g, '<');
|
| 1012 |
}
|
| 1013 |
|
| 1014 |
+
/* Delegate clicks on per-beat image buttons + final-video regen + anchor retry. */
|
| 1015 |
document.addEventListener('click', (e) => {
|
| 1016 |
+
// Final-video regen / try-another-anchor (.btn-media or .btn-stitch with data-final-seed)
|
| 1017 |
+
const finalEl = e.target.closest('[data-final-seed]');
|
| 1018 |
+
if (finalEl && !finalEl.disabled) {
|
| 1019 |
+
const sIdx = parseInt(finalEl.dataset.finalSeed ?? finalEl.dataset.stitchSeed, 10);
|
| 1020 |
+
const anchor = finalEl.dataset.anchorBeat;
|
| 1021 |
+
if (!Number.isNaN(sIdx)) {
|
| 1022 |
+
generateFinalVideo(sIdx, anchor !== undefined ? parseInt(anchor, 10) : 0);
|
| 1023 |
+
}
|
| 1024 |
+
return;
|
| 1025 |
+
}
|
| 1026 |
const finalBtn = e.target.closest('.btn-stitch');
|
| 1027 |
if (finalBtn && !finalBtn.disabled) {
|
| 1028 |
+
const sIdx = parseInt(finalBtn.dataset.stitchSeed, 10);
|
|
|
|
|
|
|
|
|
|
| 1029 |
if (!Number.isNaN(sIdx)) generateFinalVideo(sIdx);
|
| 1030 |
return;
|
| 1031 |
}
|
|
|
|
| 1036 |
if (btn.dataset.mediaBtn === 'image') {
|
| 1037 |
manualGenerateImage(sIdx, bIdx);
|
| 1038 |
}
|
|
|
|
| 1039 |
});
|
| 1040 |
|
| 1041 |
|