mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-16 20:02:20 +02:00
Photoshop-parity editor client: layers, selection/masking, brushes,
smart filters, LUT import, Quick Edit, mobile gestures, workspaces,
batch operations, and the service-worker versioning (v978->v1006).
Source: vendor/photocraft (ad1cff44) photo-editor client files, merged
onto upstream/dev (a8c147b2). Conflict resolution documented in
pr_work/A_conflicts.md; editor files taken from vendor/photocraft
(upstream's editor is an earlier snapshot of the same feature).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
4.5 KiB
Python
124 lines
4.5 KiB
Python
import json
|
|
import subprocess
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
MODULE = (ROOT / "static/js/editor/look-recipes.js").as_uri()
|
|
EDITOR = (ROOT / "static/js/galleryEditor.js").read_text(encoding="utf-8")
|
|
|
|
|
|
def _recipe_name(prompt: str):
|
|
source = textwrap.dedent(
|
|
f"""
|
|
import {{ lookRecipeSteps }} from {json.dumps(MODULE)};
|
|
const r = lookRecipeSteps({json.dumps(prompt)});
|
|
process.stdout.write(JSON.stringify(r ? {{ name: r.name, ops: r.steps.map(s => s.op) }} : null));
|
|
"""
|
|
)
|
|
out = subprocess.run(
|
|
["node", "--input-type=module", "-e", source],
|
|
capture_output=True, text=True, check=True,
|
|
)
|
|
return json.loads(out.stdout)
|
|
|
|
|
|
def _recipe_steps(prompt: str, strength=None):
|
|
opts = "" if strength is None else f", {{ strength: {strength} }}"
|
|
source = textwrap.dedent(
|
|
f"""
|
|
import {{ lookRecipeSteps }} from {json.dumps(MODULE)};
|
|
const r = lookRecipeSteps({json.dumps(prompt)}{opts});
|
|
process.stdout.write(JSON.stringify(r ? r.steps : null));
|
|
"""
|
|
)
|
|
out = subprocess.run(
|
|
["node", "--input-type=module", "-e", source],
|
|
capture_output=True, text=True, check=True,
|
|
)
|
|
return json.loads(out.stdout)
|
|
|
|
|
|
def test_named_looks_map_to_deterministic_recipes():
|
|
for prompt in [
|
|
"make it look like a Wes Anderson film",
|
|
"make it look like a National Geographic photo",
|
|
"make it look like a Kodachrome photo",
|
|
"make it look like an Ansel Adams black and white landscape",
|
|
"make it look like a 1970s film photograph",
|
|
"make it look like a vintage Polaroid",
|
|
"make it look like a film noir",
|
|
"give it a cinematic teal and orange grade",
|
|
"make it look like a golden hour portrait",
|
|
"make it a moody Scandinavian scene",
|
|
]:
|
|
recipe = _recipe_name(prompt)
|
|
assert recipe is not None, prompt
|
|
# Every step must be one of the supported deterministic control ops.
|
|
assert all(op in {"photo.autoTone", "adjustments.apply", "filters.apply"} for op in recipe["ops"]), prompt
|
|
assert any(op == "adjustments.apply" for op in recipe["ops"]), prompt
|
|
|
|
|
|
def test_content_looks_route_to_the_model_not_a_recipe():
|
|
for prompt in [
|
|
"give it a Blade Runner neon night look",
|
|
"make it a neon-lit city street",
|
|
"give it a Studio Ghibli animated look",
|
|
"make it look like an anime",
|
|
"turn it into an oil painting",
|
|
"make it look like a watercolour",
|
|
]:
|
|
assert _recipe_name(prompt) is None, prompt
|
|
|
|
|
|
def test_strength_scales_the_recipe():
|
|
base = _recipe_steps("make it look like a Kodachrome photo")
|
|
assert base is not None
|
|
strong = _recipe_steps("make it look like a Kodachrome photo", strength=2)
|
|
assert strong is not None
|
|
# The vibrance-style adjustment must scale up with strength.
|
|
def _max_numeric(steps):
|
|
best = 0.0
|
|
for s in steps:
|
|
if s.get("op") != "adjustments.apply":
|
|
continue
|
|
for v in (s.get("params", {}).get("params", {}) or {}).values():
|
|
if isinstance(v, (int, float)):
|
|
best = max(best, abs(float(v)))
|
|
return best
|
|
assert _max_numeric(strong) > _max_numeric(base)
|
|
assert _max_numeric(strong) >= 2 * _max_numeric(base) - 1
|
|
|
|
|
|
def test_non_look_prompts_are_not_intercepted():
|
|
for prompt in [
|
|
"rotate 90",
|
|
"brighten the photo",
|
|
"make it warmer",
|
|
"remove the background",
|
|
"add a man to the scene",
|
|
"make it look snowy",
|
|
"make it a square",
|
|
]:
|
|
assert _recipe_name(prompt) is None, prompt
|
|
|
|
|
|
def test_general_look_deriver_handles_unknown_named_looks():
|
|
recipe = _recipe_name("give it a warm dreamy aesthetic")
|
|
assert recipe is not None
|
|
ops = recipe["ops"]
|
|
assert "photo.autoTone" in ops
|
|
assert "adjustments.apply" in ops
|
|
|
|
|
|
def test_editor_wires_the_look_recipe_branch():
|
|
assert "import { lookRecipeSteps } from './editor/look-recipes.js';" in EDITOR
|
|
assert "const _lookRec = lookRecipeSteps(prompt, { strength: _lookStrength() });" in EDITOR
|
|
assert "for (const step of _lookRec.steps) {" in EDITOR
|
|
# Content-change looks route to the model (creative repaint), not a grade.
|
|
assert r"neon|blade\\s*runner|bloom|holograph" in EDITOR
|
|
ai_settings = (ROOT / "static/js/editor/ai-settings.js").read_text(encoding="utf-8")
|
|
assert 'id="ge-look-strength"' in ai_settings
|
|
assert "localStorage.setItem('ge-look-strength'" in ai_settings
|