plotagon-director 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,120 @@
1
+ """Decompilation d'un .plotdoc vers le format project.yaml.
2
+
3
+ Chemin inverse du generateur : permet d'editer au texte un film cree dans
4
+ l'appli, puis de le regenerer. Les types d'instructions inconnus sont
5
+ conserves bruts dans `unknown_instructions` (rien n'est perdu).
6
+ """
7
+
8
+ import re
9
+
10
+
11
+ def _slug(text):
12
+ s = re.sub(r"[^a-z0-9]+", "", str(text).lower())
13
+ return s or "actor"
14
+
15
+
16
+ def _ref_for(character_id, display_name, cast):
17
+ """Retourne un ref stable pour un personnage (meme id -> meme ref)."""
18
+ for ref, member in cast.items():
19
+ if member["character"] == character_id:
20
+ return ref
21
+ base = _slug(display_name or character_id.split(".")[-1])
22
+ ref = base
23
+ n = 2
24
+ while ref in cast:
25
+ ref = "%s%d" % (base, n)
26
+ n += 1
27
+ cast[ref] = {
28
+ "ref": ref,
29
+ "character": character_id,
30
+ "name": display_name or character_id.split(".")[-1].capitalize(),
31
+ }
32
+ return ref
33
+
34
+
35
+ def plotdoc_to_project(plotdoc):
36
+ """Transforme un dict .plotdoc en dict project.yaml.
37
+
38
+ Retourne (project, unknown_count).
39
+ """
40
+ cast = {}
41
+ scenes = []
42
+ unknown = []
43
+ current = None
44
+
45
+ for instr in plotdoc.get("contents", {}).get("instructions", []):
46
+ itype = instr.get("type")
47
+ p = instr.get("parameters", {})
48
+
49
+ if itype == "scene":
50
+ current = {"scene": p.get("scene", {}).get("id", "")}
51
+ name = p.get("scene", {}).get("text")
52
+ if name:
53
+ current["scene_name"] = name
54
+ current["actors"] = []
55
+ current["dialogue"] = []
56
+ i = 1
57
+ while ("actor%d" % i) in p:
58
+ actor = p["actor%d" % i]
59
+ ref = _ref_for(actor.get("id", ""), actor.get("text"), cast)
60
+ entry = {"ref": ref}
61
+ position = p.get("location%d" % i, {}).get("id")
62
+ if position:
63
+ entry["position"] = position
64
+ current["actors"].append(entry)
65
+ i += 1
66
+ scenes.append(current)
67
+
68
+ elif itype == "dialogue":
69
+ if current is None:
70
+ current = {"scene": "", "actors": [], "dialogue": []}
71
+ scenes.append(current)
72
+ character = p.get("character", {})
73
+ ref = _ref_for(character.get("id", ""), character.get("text"), cast)
74
+ line = {
75
+ "actor": ref,
76
+ "expression": p.get("expression", {}).get("id", "neutral"),
77
+ "text": p.get("text", {}).get("text", ""),
78
+ }
79
+ if p.get("isRecorded"):
80
+ # replique avec audio embarque dans le .plot d'origine ;
81
+ # ajouter `voice:` au cast pour la re-synthetiser au build
82
+ line["recorded"] = True
83
+ camera = p.get("camera")
84
+ if camera:
85
+ line["camera"] = camera.get("type", {}).get("name", "")
86
+ if camera.get("position"):
87
+ line["camera_position"] = camera["position"].get("id", "")
88
+ if camera.get("target"):
89
+ line["camera_target"] = camera["target"].get("id", "")
90
+ current["dialogue"].append(line)
91
+
92
+ elif itype == "music":
93
+ if current is not None:
94
+ current["music"] = p.get("music", {}).get("id", "")
95
+
96
+ elif itype == "action":
97
+ if current is None:
98
+ current = {"scene": "", "actors": [], "dialogue": []}
99
+ scenes.append(current)
100
+ character = p.get("character", {})
101
+ ref = _ref_for(character.get("id", ""), character.get("text"), cast)
102
+ line = {"actor": ref, "action": p.get("action", {}).get("id", "")}
103
+ target = p.get("target")
104
+ if target:
105
+ line["target"] = _ref_for(
106
+ target.get("id", ""), target.get("text"), cast
107
+ )
108
+ current["dialogue"].append(line)
109
+
110
+ else:
111
+ unknown.append(instr)
112
+
113
+ project = {
114
+ "project": {"title": plotdoc.get("name", "untitled")},
115
+ "cast": list(cast.values()),
116
+ "scenes": scenes,
117
+ }
118
+ if unknown:
119
+ project["unknown_instructions"] = unknown
120
+ return project, len(unknown)
@@ -0,0 +1,89 @@
1
+ """Comparaison structurelle de deux .plot — outil de retro-ingenierie.
2
+
3
+ Usage type : exporter un projet avant/apres l'ajout d'une fonctionnalite
4
+ inconnue (musique, camera, 2e acteur...) dans l'appli officielle, puis
5
+ comparer pour isoler les nouveaux types d'instructions, cles JSON et
6
+ fichiers embarques.
7
+ """
8
+
9
+ import zipfile
10
+ from collections import Counter
11
+
12
+ from .archive import read_plot
13
+
14
+
15
+ def _entry_kinds(path):
16
+ kinds = Counter()
17
+ names = []
18
+ with zipfile.ZipFile(path) as zf:
19
+ for name in zf.namelist():
20
+ suffix = "." + name.rsplit(".", 1)[-1] if "." in name else "(sans ext)"
21
+ kinds[suffix] += 1
22
+ names.append(name)
23
+ return kinds, names
24
+
25
+
26
+ def _param_keys_by_type(plotdoc):
27
+ keys = {}
28
+ for instr in plotdoc.get("contents", {}).get("instructions", []):
29
+ keys.setdefault(instr.get("type"), set()).update(
30
+ instr.get("parameters", {}).keys()
31
+ )
32
+ return keys
33
+
34
+
35
+ def diff_plots(path_a, path_b):
36
+ """Retourne un rapport texte des differences structurelles A -> B."""
37
+ lines = []
38
+ doc_a, doc_b = read_plot(path_a), read_plot(path_b)
39
+ kinds_a, _ = _entry_kinds(path_a)
40
+ kinds_b, _ = _entry_kinds(path_b)
41
+
42
+ lines.append("FICHIERS EMBARQUES (extension: A -> B)")
43
+ for suffix in sorted(set(kinds_a) | set(kinds_b)):
44
+ a, b = kinds_a.get(suffix, 0), kinds_b.get(suffix, 0)
45
+ marker = " " if a == b else "->"
46
+ lines.append(" %s %-12s %d -> %d" % (marker, suffix, a, b))
47
+
48
+ lines.append("")
49
+ lines.append("CLES RACINE DU .plotdoc")
50
+ root_a, root_b = set(doc_a), set(doc_b)
51
+ for key in sorted(root_b - root_a):
52
+ lines.append(" + %s (nouveau dans B)" % key)
53
+ for key in sorted(root_a - root_b):
54
+ lines.append(" - %s (absent de B)" % key)
55
+ if root_a == root_b:
56
+ lines.append(" identiques")
57
+
58
+ lines.append("")
59
+ lines.append("TYPES D'INSTRUCTIONS ET PARAMETRES")
60
+ keys_a = _param_keys_by_type(doc_a)
61
+ keys_b = _param_keys_by_type(doc_b)
62
+ for itype in sorted(set(keys_a) | set(keys_b)):
63
+ in_a, in_b = keys_a.get(itype), keys_b.get(itype)
64
+ if in_a is None:
65
+ lines.append(" + type '%s' NOUVEAU dans B, parametres: %s"
66
+ % (itype, ", ".join(sorted(in_b))))
67
+ elif in_b is None:
68
+ lines.append(" - type '%s' absent de B" % itype)
69
+ else:
70
+ new = in_b - in_a
71
+ gone = in_a - in_b
72
+ if new:
73
+ lines.append(" ~ type '%s': nouveaux parametres dans B: %s"
74
+ % (itype, ", ".join(sorted(new))))
75
+ if gone:
76
+ lines.append(" ~ type '%s': parametres absents de B: %s"
77
+ % (itype, ", ".join(sorted(gone))))
78
+ if not new and not gone:
79
+ lines.append(" type '%s': parametres identiques" % itype)
80
+
81
+ counts_a = Counter(i["type"] for i in doc_a["contents"]["instructions"])
82
+ counts_b = Counter(i["type"] for i in doc_b["contents"]["instructions"])
83
+ lines.append("")
84
+ lines.append("NOMBRE D'INSTRUCTIONS PAR TYPE (A -> B)")
85
+ for itype in sorted(set(counts_a) | set(counts_b)):
86
+ lines.append(" %-12s %d -> %d"
87
+ % (itype, counts_a.get(itype, 0), counts_b.get(itype, 0)))
88
+
89
+ return "\n".join(lines)
@@ -0,0 +1,67 @@
1
+ """Diagnostic d'environnement : dependances, bibliotheque, installation, TLS."""
2
+
3
+ import importlib
4
+ import sys
5
+
6
+ from ..library import DATA_PATH, load_library
7
+
8
+
9
+ def run_doctor(check_network=True):
10
+ """Retourne (rapport_texte, ok_global)."""
11
+ lines = []
12
+ ok = True
13
+
14
+ def check(label, passed, detail=""):
15
+ nonlocal ok
16
+ ok = ok and passed
17
+ lines.append("%s %-38s %s" % ("OK " if passed else "FAIL", label, detail))
18
+
19
+ check("Python >= 3.10", sys.version_info >= (3, 10),
20
+ "%d.%d" % sys.version_info[:2])
21
+
22
+ for module in ("yaml", "edge_tts", "soundfile", "numpy", "truststore"):
23
+ try:
24
+ importlib.import_module(module)
25
+ check("module %s" % module, True)
26
+ except ImportError as exc:
27
+ check("module %s" % module, False, str(exc))
28
+
29
+ library = load_library()
30
+ if library:
31
+ check("bibliotheque (library.json)", True,
32
+ "%d scenes, %d personnages, %d expressions" % (
33
+ len(library["scenes"]), len(library["characters"]),
34
+ len(library["expressions"])))
35
+ else:
36
+ check("bibliotheque (library.json)", False,
37
+ "lancer: python -m plotagon_director.library.builder (%s)" % DATA_PATH)
38
+
39
+ # optionnelle: necessaire uniquement pour RECONSTRUIRE la bibliotheque
40
+ from ..library.builder import DEFAULT_MANIFEST
41
+ check("installation Plotagon (optionnelle)", True,
42
+ str(DEFAULT_MANIFEST) if DEFAULT_MANIFEST.exists()
43
+ else "non trouvee — le generateur fonctionne sans (bibliotheque embarquee)")
44
+
45
+ if check_network:
46
+ try:
47
+ import ssl
48
+ import socket
49
+ try:
50
+ import truststore
51
+ truststore.inject_into_ssl()
52
+ except ImportError:
53
+ pass
54
+ ctx = ssl.create_default_context()
55
+ with socket.create_connection(("speech.platform.bing.com", 443), timeout=6) as s:
56
+ with ctx.wrap_socket(s, server_hostname="speech.platform.bing.com"):
57
+ pass
58
+ check("TLS vers Edge TTS", True)
59
+ except ssl.SSLCertVerificationError as exc:
60
+ check("TLS vers Edge TTS", False,
61
+ "certificat rejete (antivirus qui intercepte ? verifier truststore): %s" % exc)
62
+ except OSError as exc:
63
+ check("TLS vers Edge TTS", False, "reseau indisponible: %s" % exc)
64
+
65
+ lines.append("")
66
+ lines.append("RESULT %s" % ("PASS" if ok else "FAIL"))
67
+ return "\n".join(lines), ok
@@ -0,0 +1,178 @@
1
+ """Construction du document .plotdoc depuis un projet YAML.
2
+
3
+ Format CONFIRME le 2026-08-31 sur Plotagon Studio 1.11.0 (exports officiels).
4
+ Voir docs/PLOT_FORMAT.md pour la specification complete.
5
+ """
6
+
7
+ import uuid
8
+ from datetime import datetime
9
+
10
+ # Duree estimee par caractere de dialogue non synthetise, calibree sur
11
+ # l'echantillon officiel (63 caracteres -> lengthSeconds "6755").
12
+ MS_PER_CHAR = 107
13
+
14
+ _POSITION_TEXTS = {"Left": "on screen", "Right": "on screen"}
15
+
16
+ # Enum des types de camera — ordre releve dans Assembly-CSharp.dll et
17
+ # CONFIRME par un export officiel (4="moving right", 8="wide shot").
18
+ CAMERA_TYPES = [
19
+ "default", # 0
20
+ "establishing shot", # 1
21
+ "in dialogue mode", # 2
22
+ "in interaction mode", # 3
23
+ "moving right", # 4
24
+ "moving left", # 5
25
+ "moving forward", # 6
26
+ "moving backward", # 7
27
+ "wide shot", # 8
28
+ "still", # 9
29
+ ]
30
+
31
+ _CAMERA_POSITION_NAMES = {"EstablishingLocation": "Establishing Spot"}
32
+
33
+
34
+ def _camera_params(line, slot_texts):
35
+ """Construit le parametre camera d'une instruction dialogue."""
36
+ name = line["camera"]
37
+ camera = {"type": {"id": CAMERA_TYPES.index(name), "name": name}}
38
+ position = line.get("camera_position")
39
+ if position:
40
+ camera["position"] = {
41
+ "id": position,
42
+ "name": _CAMERA_POSITION_NAMES.get(position, position),
43
+ "useLocations": True,
44
+ }
45
+ target = line.get("camera_target")
46
+ if target:
47
+ camera["target"] = {
48
+ "id": target,
49
+ "name": slot_texts.get(target, target),
50
+ }
51
+ return camera
52
+
53
+
54
+ def _display(asset_id):
55
+ return str(asset_id).split(".")[-1].capitalize()
56
+
57
+
58
+ def _entry(asset_id, text=None):
59
+ return {"id": asset_id, "text": text if text is not None else _display(asset_id)}
60
+
61
+
62
+ def _base_params():
63
+ return {
64
+ "GUID": str(uuid.uuid4()),
65
+ "extensiondata": True,
66
+ "isRecorded": False,
67
+ "playRecording": False,
68
+ "extrasEnabled": False,
69
+ }
70
+
71
+
72
+ def build_plotdoc(data, library=None):
73
+ """Transforme un projet (dict issu de project.yaml) en dict .plotdoc.
74
+
75
+ Retourne (plotdoc, voice_jobs). voice_jobs liste les repliques a
76
+ synthetiser : {"guid", "text", "voice", "rate", "pitch"} — remplie quand
77
+ le membre du cast a un champ `voice` (id de voix Edge TTS).
78
+
79
+ library : bibliotheque (load_library()) — sert a choisir des positions
80
+ valides par defaut et leurs libelles, propres a chaque scene.
81
+ """
82
+ project = data.get("project", {})
83
+ cast = {c["ref"]: c for c in data.get("cast", [])}
84
+ all_positions = (library or {}).get("scene_positions", {})
85
+
86
+ instructions = []
87
+ voice_jobs = []
88
+ total_chars = 0
89
+
90
+ for scene in data.get("scenes", []):
91
+ params = _base_params()
92
+ params["scene"] = _entry(scene["scene"], scene.get("scene_name"))
93
+ slots = all_positions.get(scene["scene"], [])
94
+ slot_texts = {s["id"]: s["text"] for s in slots}
95
+ for i, actor in enumerate(scene.get("actors", []), start=1):
96
+ member = cast[actor["ref"]]
97
+ position = actor.get("position")
98
+ if not position:
99
+ # par defaut: i-eme slot reel de la scene, sinon Left/Right
100
+ if len(slots) >= i:
101
+ position = slots[i - 1]["id"]
102
+ else:
103
+ position = "Left" if i == 1 else "Right"
104
+ text = slot_texts.get(position) or _POSITION_TEXTS.get(
105
+ position, "on screen"
106
+ )
107
+ params["location%d" % i] = _entry(position, text)
108
+ params["actor%d" % i] = _entry(
109
+ member["character"], member.get("name")
110
+ )
111
+ instructions.append({"type": "scene", "parameters": params})
112
+
113
+ # musique de scene — structure relevee dans app.js (PROBABLE)
114
+ if scene.get("music"):
115
+ params = _base_params()
116
+ params["music"] = _entry(scene["music"], scene.get("music_name"))
117
+ instructions.append({"type": "music", "parameters": params})
118
+
119
+ for line in scene.get("dialogue", []):
120
+ member = cast[line["actor"]]
121
+ params = _base_params()
122
+
123
+ if line.get("action"):
124
+ # instruction action — CONFIRMEE par export officiel:
125
+ # {character, target, action} ; le texte est la phrase
126
+ # verbale de la bibliotheque (ex. "force chokes")
127
+ known = (library or {}).get("actions", {}).get(line["action"], {})
128
+ params["action"] = _entry(
129
+ line["action"],
130
+ line.get("action_name") or known.get("phrase"),
131
+ )
132
+ params["character"] = _entry(
133
+ member["character"], member.get("name")
134
+ )
135
+ target_ref = line.get("target")
136
+ if target_ref:
137
+ target = cast[target_ref]
138
+ params["target"] = _entry(
139
+ target["character"], target.get("name")
140
+ )
141
+ instructions.append({"type": "action", "parameters": params})
142
+ continue
143
+
144
+ expression = line.get("expression", "neutral")
145
+ params["expression"] = _entry(expression, expression.capitalize())
146
+ params["text"] = {"id": "", "text": line["text"]}
147
+ params["character"] = _entry(member["character"], member.get("name"))
148
+ if line.get("camera"):
149
+ params["camera"] = _camera_params(line, slot_texts)
150
+ voice = line.get("voice") or member.get("voice")
151
+ if voice:
152
+ params["isRecorded"] = True
153
+ params["playRecording"] = True
154
+ voice_jobs.append({
155
+ "guid": params["GUID"],
156
+ "text": line["text"],
157
+ "voice": voice,
158
+ "rate": line.get("rate") or member.get("rate"),
159
+ "pitch": line.get("pitch") or member.get("pitch"),
160
+ })
161
+ instructions.append({"type": "dialogue", "parameters": params})
162
+ total_chars += len(line["text"])
163
+
164
+ now = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
165
+ plotdoc = {
166
+ "id": str(uuid.uuid4()),
167
+ "name": project.get("title", "untitled"),
168
+ "dateCreated": now,
169
+ "dateUpdated": now,
170
+ "thumbnail": "",
171
+ "lengthSeconds": str(max(1000, total_chars * MS_PER_CHAR)),
172
+ "voicerecordings": [j["guid"] for j in voice_jobs],
173
+ "contents": {
174
+ "selectedIndex": max(0, len(instructions) - 1),
175
+ "instructions": instructions,
176
+ },
177
+ }
178
+ return plotdoc, voice_jobs
@@ -0,0 +1,165 @@
1
+ """Validation d'un project.yaml contre la bibliotheque de ressources."""
2
+
3
+ from pathlib import Path
4
+
5
+ import yaml
6
+
7
+ from ..library import load_library
8
+
9
+
10
+ def load_yaml(path):
11
+ with open(path, "r", encoding="utf-8") as f:
12
+ return yaml.safe_load(f)
13
+
14
+
15
+ def load_project(path):
16
+ """Charge un project.yaml, en fusionnant un eventuel `cast_file`.
17
+
18
+ `cast_file` (chemin relatif au project.yaml) permet de partager un cast
19
+ recurrent entre plusieurs films ; le cast local complete/precede.
20
+ """
21
+ data = load_yaml(path)
22
+ if isinstance(data, dict) and data.get("cast_file"):
23
+ extra = load_yaml(Path(path).parent / data["cast_file"]) or []
24
+ if isinstance(extra, dict):
25
+ extra = extra.get("cast", [])
26
+ data["cast"] = list(extra) + list(data.get("cast", []))
27
+ return data
28
+
29
+
30
+ def validate_project(path):
31
+ data = load_project(path)
32
+ errors, warnings = [], []
33
+
34
+ if not isinstance(data, dict):
35
+ errors.append("Project root must be a mapping.")
36
+ return errors, warnings
37
+
38
+ library = load_library()
39
+ if library is None:
40
+ warnings.append(
41
+ "library.json absent: lance `python -m plotagon_director.library.builder` "
42
+ "pour valider contre les vraies ressources."
43
+ )
44
+
45
+ project = data.get("project", {})
46
+ cast = data.get("cast", [])
47
+ scenes = data.get("scenes", [])
48
+
49
+ if not project.get("title"):
50
+ warnings.append("Project title is missing.")
51
+ if not scenes:
52
+ errors.append("No scenes defined.")
53
+
54
+ refs = {}
55
+ for member in cast:
56
+ if not isinstance(member, dict) or not member.get("ref"):
57
+ errors.append("Cast member without ref.")
58
+ continue
59
+ if member["ref"] in refs:
60
+ errors.append("Duplicate cast ref '%s'." % member["ref"])
61
+ refs[member["ref"]] = member
62
+ character = member.get("character")
63
+ if not character:
64
+ errors.append("Cast '%s': missing character id." % member["ref"])
65
+ elif library and character not in library["characters"]:
66
+ errors.append(
67
+ "Cast '%s': character '%s' absent de la bibliotheque."
68
+ % (member["ref"], character)
69
+ )
70
+
71
+ for index, scene in enumerate(scenes, start=1):
72
+ sid = scene.get("scene", "?")
73
+ label = "Scene %d (%s)" % (index, sid)
74
+
75
+ if not scene.get("scene"):
76
+ errors.append("Scene %d: missing scene id." % index)
77
+ elif library and sid not in library["scenes"]:
78
+ errors.append("%s: scene absente de la bibliotheque." % label)
79
+
80
+ actors = scene.get("actors", [])
81
+ if not actors:
82
+ warnings.append("%s: no actors." % label)
83
+ if len(actors) > 2:
84
+ warnings.append(
85
+ "%s: %d acteurs — seuls 2 slots (actor1/actor2) sont CONFIRMES."
86
+ % (label, len(actors))
87
+ )
88
+ slots = (library or {}).get("scene_positions", {}).get(sid, [])
89
+ slot_ids = {s["id"] for s in slots}
90
+ if slots and len(actors) > len(slots):
91
+ warnings.append(
92
+ "%s: %d acteurs pour %d position(s) disponible(s)."
93
+ % (label, len(actors), len(slots))
94
+ )
95
+ for actor in actors:
96
+ if actor.get("ref") not in refs:
97
+ errors.append("%s: unknown actor ref '%s'." % (label, actor.get("ref")))
98
+ position = actor.get("position")
99
+ if position and slot_ids and position not in slot_ids:
100
+ errors.append(
101
+ "%s: position '%s' inexistante (disponibles: %s)."
102
+ % (label, position, ", ".join(sorted(slot_ids)))
103
+ )
104
+
105
+ music = scene.get("music")
106
+ if music and music != "music.stopmusic":
107
+ if library and music not in library["music"]:
108
+ errors.append(
109
+ "%s: musique '%s' absente de la bibliotheque." % (label, music)
110
+ )
111
+
112
+ for line in scene.get("dialogue", []):
113
+ if line.get("actor") not in refs:
114
+ errors.append(
115
+ "%s: dialogue actor '%s' is unknown." % (label, line.get("actor"))
116
+ )
117
+
118
+ if line.get("action"):
119
+ action_id = line["action"]
120
+ known_actions = (library or {}).get("actions", {})
121
+ if known_actions and action_id not in known_actions:
122
+ errors.append(
123
+ "%s: action '%s' absente de la bibliotheque "
124
+ "(valides: %s)."
125
+ % (label, action_id, ", ".join(sorted(known_actions)))
126
+ )
127
+ target = line.get("target")
128
+ if target and target not in refs:
129
+ errors.append(
130
+ "%s: action target '%s' is unknown." % (label, target)
131
+ )
132
+ if not target:
133
+ warnings.append(
134
+ "%s: action '%s' sans target — les actions sont des "
135
+ "interactions a 2 personnages (observees avec target)."
136
+ % (label, action_id)
137
+ )
138
+ continue
139
+
140
+ if not line.get("text"):
141
+ # valide dans l'appli (mime sans parole), observe dans les
142
+ # exports officiels — simple avertissement
143
+ warnings.append("%s: dialogue sans texte (mime)." % label)
144
+ expression = line.get("expression", "neutral")
145
+ if library and expression not in library["expressions"]:
146
+ errors.append(
147
+ "%s: expression '%s' absente de la bibliotheque."
148
+ % (label, expression)
149
+ )
150
+ camera = line.get("camera")
151
+ if camera:
152
+ from .plotdoc import CAMERA_TYPES
153
+ if camera not in CAMERA_TYPES:
154
+ errors.append(
155
+ "%s: camera '%s' inconnue (valides: %s)."
156
+ % (label, camera, ", ".join(CAMERA_TYPES))
157
+ )
158
+ target = line.get("camera_target")
159
+ if target and slot_ids and target not in slot_ids:
160
+ warnings.append(
161
+ "%s: camera_target '%s' n'est pas un slot de la scene."
162
+ % (label, target)
163
+ )
164
+
165
+ return errors, warnings
@@ -0,0 +1,14 @@
1
+ """Bibliotheque des ressources Plotagon (scenes, personnages, expressions...)."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ DATA_PATH = Path(__file__).parent / "data" / "library.json"
7
+
8
+
9
+ def load_library():
10
+ """Retourne la bibliotheque, ou None si elle n'a pas ete construite."""
11
+ if DATA_PATH.exists():
12
+ with open(DATA_PATH, encoding="utf-8") as f:
13
+ return json.load(f)
14
+ return None