plotagon-director 0.9.2__tar.gz → 0.10.0__tar.gz

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.
Files changed (32) hide show
  1. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/PKG-INFO +11 -2
  2. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/README.md +10 -1
  3. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/__init__.py +3 -3
  4. plotagon_director-0.10.0/plotagon_director/casting.py +135 -0
  5. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/cli.py +41 -4
  6. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/core/archive.py +5 -1
  7. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/core/validator.py +25 -1
  8. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/mcp_server.py +17 -0
  9. plotagon_director-0.10.0/plotagon_director/schemas/project.schema.json +217 -0
  10. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director.egg-info/PKG-INFO +11 -2
  11. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director.egg-info/SOURCES.txt +1 -0
  12. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/pyproject.toml +52 -52
  13. plotagon_director-0.9.2/plotagon_director/schemas/project.schema.json +0 -105
  14. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/LICENSE +0 -0
  15. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/audio/__init__.py +0 -0
  16. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/audio/phonemes.py +0 -0
  17. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/audio/tts.py +0 -0
  18. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/core/__init__.py +0 -0
  19. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/core/decompiler.py +0 -0
  20. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/core/diffplot.py +0 -0
  21. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/core/doctor.py +0 -0
  22. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/core/plotdoc.py +0 -0
  23. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/http_api.py +0 -0
  24. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/library/__init__.py +0 -0
  25. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/library/builder.py +0 -0
  26. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/library/data/library.json +0 -0
  27. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director/storyteller.py +0 -0
  28. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director.egg-info/dependency_links.txt +0 -0
  29. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director.egg-info/entry_points.txt +0 -0
  30. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director.egg-info/requires.txt +0 -0
  31. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/plotagon_director.egg-info/top_level.txt +0 -0
  32. {plotagon_director-0.9.2 → plotagon_director-0.10.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotagon-director
3
- Version: 0.9.2
3
+ Version: 0.10.0
4
4
  Summary: Génération de films Plotagon Studio Desktop (.plot) avec voix Edge TTS et synchro labiale
5
5
  Author: Mohamed Amine Ben Mallessa
6
6
  License: MIT
@@ -90,6 +90,13 @@ plotagon-director batch scenarios/ sorties/ # tous les .yaml d'un dossier
90
90
 
91
91
  # Importer mon-film.plot dans Plotagon Studio (menu import)
92
92
 
93
+ # Personnages custom (créés dans le créateur de Plotagon Studio)
94
+ plotagon-director casting add export.plot # récolte les persos d'un export
95
+ plotagon-director casting list [--json] # registre local (./casting)
96
+ # puis dans le cast du yaml : `custom: Leo` au lieu de `character: ...`
97
+ # (clé racine `casting_dir:` optionnelle ; défaut ./casting, sinon
98
+ # variable PLOTAGON_CASTING_DIR) — voix TTS et synchro labiale incluses
99
+
93
100
  # Analyse / rétro-ingénierie
94
101
  plotagon-director parse export.plot # JSON brut d'un .plot
95
102
  plotagon-director decompile export.plot film.yaml # .plot -> YAML éditable
@@ -165,6 +172,8 @@ Bibliothèque embarquée : 205 scènes (953 positions), 53 personnages,
165
172
  - `plotagon_director/audio/` — Edge TTS → WAV conforme (cache + parallèle),
166
173
  phonèmes/lip-sync
167
174
  - `plotagon_director/library/` — builder du catalogue + `data/library.json`
175
+ - `plotagon_director/casting.py` — registre des personnages custom
176
+ (récolte des `.pcd` d'un export, résolution du cast `custom:`)
168
177
  - `plotagon_director/storyteller.py` — pipeline vidéo/transcription → .plot
169
178
  (`plotagon-storyteller`)
170
179
  - `plotagon_director/mcp_server.py` / `http_api.py` — serveur MCP
@@ -193,7 +202,7 @@ Le projet expose plusieurs surfaces pour les agents :
193
202
  - **Serveur MCP** : `pip install plotagon-director[mcp]` puis
194
203
  `plotagon-director mcp` (stdio). Outils : search_library, list_category,
195
204
  scene_positions, list_voices, validate_project, preview_durations,
196
- build_film, storytell. Compatible Claude Desktop/Code, Cursor, VS Code,
205
+ build_film, storytell, casting_add, casting_list. Compatible Claude Desktop/Code, Cursor, VS Code,
197
206
  Gemini CLI... Exemple de config client :
198
207
  `{"command": "plotagon-director", "args": ["mcp"]}`
199
208
  - **API HTTP + OpenAPI** : `pip install plotagon-director[http]` puis
@@ -51,6 +51,13 @@ plotagon-director batch scenarios/ sorties/ # tous les .yaml d'un dossier
51
51
 
52
52
  # Importer mon-film.plot dans Plotagon Studio (menu import)
53
53
 
54
+ # Personnages custom (créés dans le créateur de Plotagon Studio)
55
+ plotagon-director casting add export.plot # récolte les persos d'un export
56
+ plotagon-director casting list [--json] # registre local (./casting)
57
+ # puis dans le cast du yaml : `custom: Leo` au lieu de `character: ...`
58
+ # (clé racine `casting_dir:` optionnelle ; défaut ./casting, sinon
59
+ # variable PLOTAGON_CASTING_DIR) — voix TTS et synchro labiale incluses
60
+
54
61
  # Analyse / rétro-ingénierie
55
62
  plotagon-director parse export.plot # JSON brut d'un .plot
56
63
  plotagon-director decompile export.plot film.yaml # .plot -> YAML éditable
@@ -126,6 +133,8 @@ Bibliothèque embarquée : 205 scènes (953 positions), 53 personnages,
126
133
  - `plotagon_director/audio/` — Edge TTS → WAV conforme (cache + parallèle),
127
134
  phonèmes/lip-sync
128
135
  - `plotagon_director/library/` — builder du catalogue + `data/library.json`
136
+ - `plotagon_director/casting.py` — registre des personnages custom
137
+ (récolte des `.pcd` d'un export, résolution du cast `custom:`)
129
138
  - `plotagon_director/storyteller.py` — pipeline vidéo/transcription → .plot
130
139
  (`plotagon-storyteller`)
131
140
  - `plotagon_director/mcp_server.py` / `http_api.py` — serveur MCP
@@ -154,7 +163,7 @@ Le projet expose plusieurs surfaces pour les agents :
154
163
  - **Serveur MCP** : `pip install plotagon-director[mcp]` puis
155
164
  `plotagon-director mcp` (stdio). Outils : search_library, list_category,
156
165
  scene_positions, list_voices, validate_project, preview_durations,
157
- build_film, storytell. Compatible Claude Desktop/Code, Cursor, VS Code,
166
+ build_film, storytell, casting_add, casting_list. Compatible Claude Desktop/Code, Cursor, VS Code,
158
167
  Gemini CLI... Exemple de config client :
159
168
  `{"command": "plotagon-director", "args": ["mcp"]}`
160
169
  - **API HTTP + OpenAPI** : `pip install plotagon-director[http]` puis
@@ -1,3 +1,3 @@
1
- """plotagon_director — generation de films Plotagon Studio Desktop (.plot)."""
2
-
3
- __version__ = "0.9.2"
1
+ """plotagon_director — generation de films Plotagon Studio Desktop (.plot)."""
2
+
3
+ __version__ = "0.10.0"
@@ -0,0 +1,135 @@
1
+ """Personnages custom : registre local et integration au build.
2
+
3
+ Un personnage custom est cree dans le createur de Plotagon Studio puis
4
+ exporte dans un .plot : l'archive contient "customcharacters/<Nom>_<guid>.pcd"
5
+ (recette JSON "TmpL" + geometrie 3D bakee) et sa vignette .png. Le registre
6
+ local (un dossier + registry.json) conserve ces fichiers pour les reutiliser
7
+ dans les films generes : il suffit de referencer le GUID dans les
8
+ instructions et d'embarquer les fichiers dans l'archive.
9
+
10
+ Resolution du dossier de casting (premier trouve) :
11
+ 1. cle `casting_dir` du project.yaml (absolue, ou relative au yaml) ;
12
+ 2. dossier `casting/` a cote du project.yaml ;
13
+ 3. variable d'environnement PLOTAGON_CASTING_DIR.
14
+ """
15
+
16
+ import json
17
+ import os
18
+ import zipfile
19
+ from pathlib import Path
20
+
21
+ REGISTRY_NAME = "registry.json"
22
+
23
+
24
+ def parse_recipe(pcd_bytes):
25
+ """Extrait la recette JSON (chunk TmpL) d'un fichier .pcd."""
26
+ start = pcd_bytes.find(b"{")
27
+ if start < 0:
28
+ raise ValueError("recette TmpL introuvable dans le .pcd")
29
+ depth = 0
30
+ for i in range(start, len(pcd_bytes)):
31
+ char = pcd_bytes[i:i + 1]
32
+ if char == b"{":
33
+ depth += 1
34
+ elif char == b"}":
35
+ depth -= 1
36
+ if depth == 0:
37
+ return json.loads(pcd_bytes[start:i + 1])
38
+ raise ValueError("recette TmpL tronquee dans le .pcd")
39
+
40
+
41
+ def casting_dir(data=None, project_path=None):
42
+ """Retourne le dossier de casting applicable (Path) ou None."""
43
+ if isinstance(data, dict) and data.get("casting_dir"):
44
+ base = Path(project_path).parent if project_path else Path(".")
45
+ path = Path(data["casting_dir"])
46
+ return path if path.is_absolute() else base / path
47
+ if project_path:
48
+ local = Path(project_path).parent / "casting"
49
+ if local.is_dir():
50
+ return local
51
+ env = os.environ.get("PLOTAGON_CASTING_DIR")
52
+ if env:
53
+ return Path(env)
54
+ return None
55
+
56
+
57
+ def load_registry(directory):
58
+ """Charge registry.json d'un dossier de casting -> {nom: entree}."""
59
+ if not directory:
60
+ return {}
61
+ path = Path(directory) / REGISTRY_NAME
62
+ if not path.is_file():
63
+ return {}
64
+ with open(path, "r", encoding="utf-8") as f:
65
+ return json.load(f)
66
+
67
+
68
+ def harvest(plot_path, directory):
69
+ """Recolte les personnages custom d'un .plot exporte vers le registre.
70
+
71
+ Copie les customcharacters/*.pcd|png dans `directory` et met a jour
72
+ registry.json. Retourne la liste des noms ajoutes/mis a jour.
73
+ """
74
+ directory = Path(directory)
75
+ directory.mkdir(parents=True, exist_ok=True)
76
+ registry = load_registry(directory)
77
+ added = []
78
+ with zipfile.ZipFile(plot_path) as zf:
79
+ for name in zf.namelist():
80
+ if not name.startswith("customcharacters/"):
81
+ continue
82
+ filename = name.split("/", 1)[1]
83
+ payload = zf.read(name)
84
+ (directory / filename).write_bytes(payload)
85
+ if filename.endswith(".pcd"):
86
+ recipe = parse_recipe(payload)
87
+ registry[recipe["name"]] = {
88
+ "id": recipe["id"],
89
+ "gender": recipe.get("gender", ""),
90
+ "pcd": filename,
91
+ "png": filename[:-4] + ".png",
92
+ "definition": recipe.get("definition", {}),
93
+ }
94
+ added.append(recipe["name"])
95
+ with open(directory / REGISTRY_NAME, "w", encoding="utf-8") as f:
96
+ json.dump(registry, f, indent=1, ensure_ascii=False)
97
+ return added
98
+
99
+
100
+ def apply_casting(data, project_path=None):
101
+ """Resout les membres du cast declares avec `custom: <Nom>`.
102
+
103
+ Mutations en place : `character` devient le GUID du personnage et `name`
104
+ prend le nom du registre par defaut. Retourne les fichiers a embarquer
105
+ dans l'archive : {chemin_zip: bytes}. Leve ValueError si un nom est
106
+ introuvable dans le registre.
107
+ """
108
+ members = [m for m in (data or {}).get("cast", [])
109
+ if isinstance(m, dict) and m.get("custom")]
110
+ if not members:
111
+ return {}
112
+ directory = casting_dir(data, project_path)
113
+ registry = load_registry(directory)
114
+ if not registry:
115
+ raise ValueError(
116
+ "Cast custom mais aucun registre trouve (casting_dir, dossier "
117
+ "casting/ a cote du yaml, ou PLOTAGON_CASTING_DIR). Recoltez "
118
+ "d'abord un export: plotagon-director casting add <export.plot>"
119
+ )
120
+ extra_files = {}
121
+ for member in members:
122
+ name = member["custom"]
123
+ entry = registry.get(name)
124
+ if entry is None:
125
+ raise ValueError(
126
+ "Personnage custom '%s' absent du registre %s (disponibles: "
127
+ "%s)." % (name, directory, ", ".join(sorted(registry)))
128
+ )
129
+ member["character"] = entry["id"]
130
+ member.setdefault("name", name)
131
+ for key in ("pcd", "png"):
132
+ filename = entry[key]
133
+ path = Path(directory) / filename
134
+ extra_files["customcharacters/" + filename] = path.read_bytes()
135
+ return extra_files
@@ -108,7 +108,10 @@ def do_build(project_path, output, no_voices=False, strict=False, srt=None,
108
108
  errors, warnings = validate_project(project_path)
109
109
  if errors or (strict and warnings):
110
110
  print_report(errors, warnings, strict)
111
- plotdoc, voice_jobs = build_plotdoc(load_project(project_path), load_library())
111
+ from .casting import apply_casting
112
+ data = load_project(project_path)
113
+ extra_files = apply_casting(data, project_path)
114
+ plotdoc, voice_jobs = build_plotdoc(data, load_library())
112
115
 
113
116
  media = {}
114
117
  durations = {}
@@ -137,9 +140,10 @@ def do_build(project_path, output, no_voices=False, strict=False, srt=None,
137
140
  instr["parameters"]["isRecorded"] = False
138
141
  instr["parameters"]["playRecording"] = False
139
142
 
140
- write_plot(plotdoc, output, media)
141
- print("OK %s (%d instructions, %d voix)" % (
142
- output, len(plotdoc["contents"]["instructions"]), len(media)
143
+ write_plot(plotdoc, output, media, extra_files=extra_files)
144
+ print("OK %s (%d instructions, %d voix%s)" % (
145
+ output, len(plotdoc["contents"]["instructions"]), len(media),
146
+ ", %d perso(s) custom" % (len(extra_files) // 2) if extra_files else "",
143
147
  ))
144
148
 
145
149
  if srt:
@@ -346,6 +350,19 @@ def main():
346
350
  "preview", help="durees estimees d'un project.yaml sans builder")
347
351
  pprev.add_argument("project")
348
352
 
353
+ pcast = sub.add_parser(
354
+ "casting",
355
+ help="registre des personnages custom (crees dans l'appli)")
356
+ cast_sub = pcast.add_subparsers(dest="casting_command", required=True)
357
+ pcadd = cast_sub.add_parser(
358
+ "add", help="recolte les personnages custom d'un .plot exporte")
359
+ pcadd.add_argument("plot")
360
+ pcadd.add_argument("--dir", dest="directory", default="casting",
361
+ help="dossier du registre (defaut: ./casting)")
362
+ pclist = cast_sub.add_parser("list", help="liste le registre de casting")
363
+ pclist.add_argument("--dir", dest="directory", default="casting")
364
+ pclist.add_argument("--json", action="store_true", dest="as_json")
365
+
349
366
  sub.add_parser(
350
367
  "storytell", add_help=False,
351
368
  help="pipeline video/transcription -> .plot (alias plotagon-storyteller)")
@@ -463,6 +480,26 @@ def main():
463
480
  elif args.command == "preview":
464
481
  do_preview(args.project)
465
482
 
483
+ elif args.command == "casting":
484
+ from .casting import harvest, load_registry
485
+ if args.casting_command == "add":
486
+ added = harvest(args.plot, args.directory)
487
+ if added:
488
+ print("OK %d personnage(s) dans %s : %s" % (
489
+ len(added), args.directory, ", ".join(added)))
490
+ else:
491
+ print("Aucun personnage custom dans %s." % args.plot)
492
+ elif args.casting_command == "list":
493
+ registry = load_registry(args.directory)
494
+ if args.as_json:
495
+ print(json.dumps(registry, indent=1, ensure_ascii=False))
496
+ elif not registry:
497
+ print("Registre vide ou absent (%s)." % args.directory)
498
+ else:
499
+ for name, entry in sorted(registry.items()):
500
+ print("%-16s %-7s %s" % (name, entry.get("gender", "?"),
501
+ entry["id"]))
502
+
466
503
  elif args.command == "say":
467
504
  from .audio.tts import synthesize_wav
468
505
  wav = synthesize_wav(args.text, args.voice)
@@ -9,10 +9,12 @@ import json
9
9
  import zipfile
10
10
 
11
11
 
12
- def write_plot(plotdoc, out_path, media=None):
12
+ def write_plot(plotdoc, out_path, media=None, extra_files=None):
13
13
  """Ecrit un .plot.
14
14
 
15
15
  media : dict {guid_instruction: {"wav": bytes, "phonemes": bytes}}.
16
+ extra_files : dict {chemin_dans_le_zip: bytes} — p. ex. les
17
+ customcharacters/*.pcd|png des personnages custom.
16
18
  """
17
19
  inner_name = "%s.plotdoc" % plotdoc["id"]
18
20
  payload = json.dumps(plotdoc, ensure_ascii=False, indent=2)
@@ -23,6 +25,8 @@ def write_plot(plotdoc, out_path, media=None):
23
25
  zf.writestr(base + ".wav", files["wav"])
24
26
  if files.get("phonemes"):
25
27
  zf.writestr(base + ".phonemes", files["phonemes"])
28
+ for name, payload_bytes in (extra_files or {}).items():
29
+ zf.writestr(name, payload_bytes)
26
30
 
27
31
 
28
32
  def read_plot(path):
@@ -59,6 +59,9 @@ def validate_project(path):
59
59
  if not scenes:
60
60
  errors.append("No scenes defined.")
61
61
 
62
+ from ..casting import casting_dir, load_registry
63
+ registry = load_registry(casting_dir(data, path))
64
+
62
65
  refs = {}
63
66
  for member in cast:
64
67
  if not isinstance(member, dict) or not member.get("ref"):
@@ -68,7 +71,28 @@ def validate_project(path):
68
71
  errors.append("Duplicate cast ref '%s'." % member["ref"])
69
72
  refs[member["ref"]] = member
70
73
  character = member.get("character")
71
- if not character:
74
+ custom = member.get("custom")
75
+ if custom:
76
+ if character:
77
+ errors.append(
78
+ "Cast '%s': 'character' et 'custom' sont exclusifs."
79
+ % member["ref"]
80
+ )
81
+ elif not registry:
82
+ errors.append(
83
+ "Cast '%s': personnage custom '%s' mais aucun registre "
84
+ "de casting trouve (casting_dir, dossier casting/ a cote "
85
+ "du yaml, ou PLOTAGON_CASTING_DIR)."
86
+ % (member["ref"], custom)
87
+ )
88
+ elif custom not in registry:
89
+ errors.append(
90
+ "Cast '%s': personnage custom '%s' absent du registre "
91
+ "(disponibles: %s).%s"
92
+ % (member["ref"], custom, ", ".join(sorted(registry)),
93
+ _did_you_mean(custom, registry))
94
+ )
95
+ elif not character:
72
96
  errors.append("Cast '%s': missing character id." % member["ref"])
73
97
  elif library and character not in library["characters"]:
74
98
  errors.append(
@@ -122,6 +122,23 @@ def create_server():
122
122
  "errors": _structured(errors),
123
123
  "warnings": _structured(warnings)}
124
124
 
125
+ @server.tool()
126
+ def casting_add(plot_path: str, directory: str) -> dict:
127
+ """Recolte les personnages custom d'un .plot exporte depuis Plotagon
128
+ Studio vers un registre de casting local (dossier + registry.json).
129
+ Ils deviennent utilisables dans le cast via `custom: <Nom>` (avec
130
+ `casting_dir` absolu dans le project.yaml)."""
131
+ from .casting import harvest
132
+ return {"added": harvest(plot_path, directory)}
133
+
134
+ @server.tool()
135
+ def casting_list(directory: str) -> dict:
136
+ """Liste le registre de casting local {nom: {id, gender, ...}}."""
137
+ from .casting import load_registry
138
+ registry = load_registry(directory)
139
+ return {name: {"id": e["id"], "gender": e.get("gender", "")}
140
+ for name, e in registry.items()}
141
+
125
142
  @server.tool()
126
143
  def preview_durations(yaml_text: str) -> dict:
127
144
  """Durees estimees par replique et totale d'un project.yaml, sans
@@ -0,0 +1,217 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/mohamed-amine-ben-mallessa/plotagon-director/blob/main/schemas/project.schema.json",
4
+ "title": "Plotagon Director project",
5
+ "description": "Scenario YAML pour plotagon-director. Les IDs (scene, character, expression, music, action, position) doivent exister dans la bibliotheque embarquee : les valider avec `plotagon-director validate --strict` ou explorer avec `plotagon-director search/list/positions`.",
6
+ "type": "object",
7
+ "required": [
8
+ "project",
9
+ "cast",
10
+ "scenes"
11
+ ],
12
+ "additionalProperties": false,
13
+ "properties": {
14
+ "project": {
15
+ "type": "object",
16
+ "required": [
17
+ "title"
18
+ ],
19
+ "additionalProperties": false,
20
+ "properties": {
21
+ "title": {
22
+ "type": "string",
23
+ "minLength": 1
24
+ },
25
+ "language": {
26
+ "type": "string",
27
+ "description": "Locale BCP 47, ex. fr-FR, en-US",
28
+ "pattern": "^[a-z]{2}(-[A-Z]{2})?$",
29
+ "default": "fr-FR"
30
+ }
31
+ }
32
+ },
33
+ "cast_file": {
34
+ "type": "string",
35
+ "description": "Chemin (relatif au project.yaml) d'un cast partage ; fusionne avant le cast local"
36
+ },
37
+ "cast": {
38
+ "type": "array",
39
+ "items": {
40
+ "type": "object",
41
+ "required": [
42
+ "ref"
43
+ ],
44
+ "additionalProperties": false,
45
+ "properties": {
46
+ "ref": {
47
+ "type": "string",
48
+ "minLength": 1,
49
+ "description": "Identifiant local reference par scenes[].actors et dialogue[].actor"
50
+ },
51
+ "character": {
52
+ "type": "string",
53
+ "pattern": "^[a-z0-9]+\\.[a-z0-9]+$",
54
+ "description": "ID bibliotheque, ex. office.steve (53 disponibles : `plotagon-director list characters`)"
55
+ },
56
+ "name": {
57
+ "type": "string",
58
+ "description": "Nom affiche dans le film"
59
+ },
60
+ "voice": {
61
+ "type": "string",
62
+ "pattern": "^[a-z]{2}-[A-Z]{2}-[A-Za-z]+Neural$",
63
+ "description": "Voix Edge TTS, ex. fr-FR-HenriNeural (docs/VOICES.md)"
64
+ },
65
+ "rate": {
66
+ "type": "string",
67
+ "pattern": "^[+-]\\d+%$",
68
+ "description": "Debit TTS, ex. +10%"
69
+ },
70
+ "pitch": {
71
+ "type": "string",
72
+ "pattern": "^[+-]\\d+Hz$",
73
+ "description": "Hauteur TTS, ex. -5Hz"
74
+ },
75
+ "custom": {
76
+ "type": "string",
77
+ "description": "Nom d'un personnage custom du registre de casting (exclusif avec `character`). Cree dans le createur de Plotagon Studio puis recolte via `plotagon-director casting add`."
78
+ }
79
+ },
80
+ "anyOf": [
81
+ {
82
+ "required": [
83
+ "character"
84
+ ]
85
+ },
86
+ {
87
+ "required": [
88
+ "custom"
89
+ ]
90
+ }
91
+ ]
92
+ }
93
+ },
94
+ "scenes": {
95
+ "type": "array",
96
+ "minItems": 1,
97
+ "items": {
98
+ "type": "object",
99
+ "required": [
100
+ "scene"
101
+ ],
102
+ "additionalProperties": false,
103
+ "properties": {
104
+ "scene": {
105
+ "type": "string",
106
+ "pattern": "^[a-z0-9]+\\.[a-z0-9]+$",
107
+ "description": "ID bibliotheque, ex. office.ceooffice (205 disponibles)"
108
+ },
109
+ "music": {
110
+ "type": "string",
111
+ "pattern": "^music\\.[a-z0-9]+$",
112
+ "description": "38 pistes + music.stopmusic"
113
+ },
114
+ "actors": {
115
+ "type": "array",
116
+ "maxItems": 2,
117
+ "items": {
118
+ "type": "object",
119
+ "required": [
120
+ "ref"
121
+ ],
122
+ "additionalProperties": false,
123
+ "properties": {
124
+ "ref": {
125
+ "type": "string"
126
+ },
127
+ "position": {
128
+ "type": "string",
129
+ "description": "Slot propre a la scene (`plotagon-director positions <scene>`) ; omis = premier slot"
130
+ }
131
+ }
132
+ }
133
+ },
134
+ "dialogue": {
135
+ "type": "array",
136
+ "items": {
137
+ "oneOf": [
138
+ {
139
+ "type": "object",
140
+ "required": [
141
+ "actor",
142
+ "text"
143
+ ],
144
+ "additionalProperties": false,
145
+ "properties": {
146
+ "actor": {
147
+ "type": "string"
148
+ },
149
+ "expression": {
150
+ "type": "string",
151
+ "default": "neutral",
152
+ "description": "88 expressions (`plotagon-director list expressions`)"
153
+ },
154
+ "text": {
155
+ "type": "string",
156
+ "description": "Vide accepte (mime sans parole)"
157
+ },
158
+ "camera": {
159
+ "type": "string",
160
+ "enum": [
161
+ "default",
162
+ "establishing shot",
163
+ "in dialogue mode",
164
+ "in interaction mode",
165
+ "moving right",
166
+ "moving left",
167
+ "moving forward",
168
+ "moving backward",
169
+ "wide shot",
170
+ "still"
171
+ ],
172
+ "description": "'in dialogue/interaction mode' exigent 2 acteurs ; 'moving *' exigent camera_position ET camera_target"
173
+ },
174
+ "camera_position": {
175
+ "type": "string",
176
+ "description": "ex. EstablishingLocation"
177
+ },
178
+ "camera_target": {
179
+ "type": "string",
180
+ "description": "Slot de position de la scene"
181
+ }
182
+ }
183
+ },
184
+ {
185
+ "type": "object",
186
+ "required": [
187
+ "actor",
188
+ "action",
189
+ "target"
190
+ ],
191
+ "additionalProperties": false,
192
+ "properties": {
193
+ "actor": {
194
+ "type": "string"
195
+ },
196
+ "action": {
197
+ "type": "string",
198
+ "description": "15 interactions a 2 personnages (`plotagon-director list actions`)"
199
+ },
200
+ "target": {
201
+ "type": "string",
202
+ "description": "ref de l'autre acteur"
203
+ }
204
+ }
205
+ }
206
+ ]
207
+ }
208
+ }
209
+ }
210
+ }
211
+ },
212
+ "casting_dir": {
213
+ "type": "string",
214
+ "description": "Dossier du registre de personnages custom (registry.json + .pcd/.png recoltes via `plotagon-director casting add <export.plot>`). Absolu, ou relatif au project.yaml. Defauts : dossier casting/ a cote du yaml, puis variable PLOTAGON_CASTING_DIR."
215
+ }
216
+ }
217
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotagon-director
3
- Version: 0.9.2
3
+ Version: 0.10.0
4
4
  Summary: Génération de films Plotagon Studio Desktop (.plot) avec voix Edge TTS et synchro labiale
5
5
  Author: Mohamed Amine Ben Mallessa
6
6
  License: MIT
@@ -90,6 +90,13 @@ plotagon-director batch scenarios/ sorties/ # tous les .yaml d'un dossier
90
90
 
91
91
  # Importer mon-film.plot dans Plotagon Studio (menu import)
92
92
 
93
+ # Personnages custom (créés dans le créateur de Plotagon Studio)
94
+ plotagon-director casting add export.plot # récolte les persos d'un export
95
+ plotagon-director casting list [--json] # registre local (./casting)
96
+ # puis dans le cast du yaml : `custom: Leo` au lieu de `character: ...`
97
+ # (clé racine `casting_dir:` optionnelle ; défaut ./casting, sinon
98
+ # variable PLOTAGON_CASTING_DIR) — voix TTS et synchro labiale incluses
99
+
93
100
  # Analyse / rétro-ingénierie
94
101
  plotagon-director parse export.plot # JSON brut d'un .plot
95
102
  plotagon-director decompile export.plot film.yaml # .plot -> YAML éditable
@@ -165,6 +172,8 @@ Bibliothèque embarquée : 205 scènes (953 positions), 53 personnages,
165
172
  - `plotagon_director/audio/` — Edge TTS → WAV conforme (cache + parallèle),
166
173
  phonèmes/lip-sync
167
174
  - `plotagon_director/library/` — builder du catalogue + `data/library.json`
175
+ - `plotagon_director/casting.py` — registre des personnages custom
176
+ (récolte des `.pcd` d'un export, résolution du cast `custom:`)
168
177
  - `plotagon_director/storyteller.py` — pipeline vidéo/transcription → .plot
169
178
  (`plotagon-storyteller`)
170
179
  - `plotagon_director/mcp_server.py` / `http_api.py` — serveur MCP
@@ -193,7 +202,7 @@ Le projet expose plusieurs surfaces pour les agents :
193
202
  - **Serveur MCP** : `pip install plotagon-director[mcp]` puis
194
203
  `plotagon-director mcp` (stdio). Outils : search_library, list_category,
195
204
  scene_positions, list_voices, validate_project, preview_durations,
196
- build_film, storytell. Compatible Claude Desktop/Code, Cursor, VS Code,
205
+ build_film, storytell, casting_add, casting_list. Compatible Claude Desktop/Code, Cursor, VS Code,
197
206
  Gemini CLI... Exemple de config client :
198
207
  `{"command": "plotagon-director", "args": ["mcp"]}`
199
208
  - **API HTTP + OpenAPI** : `pip install plotagon-director[http]` puis
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  plotagon_director/__init__.py
5
+ plotagon_director/casting.py
5
6
  plotagon_director/cli.py
6
7
  plotagon_director/http_api.py
7
8
  plotagon_director/mcp_server.py
@@ -1,52 +1,52 @@
1
- [build-system]
2
- requires = ["setuptools>=68"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "plotagon-director"
7
- version = "0.9.2"
8
- description = "Génération de films Plotagon Studio Desktop (.plot) avec voix Edge TTS et synchro labiale"
9
- readme = "README.md"
10
- license = { text = "MIT" }
11
- authors = [{ name = "Mohamed Amine Ben Mallessa" }]
12
- requires-python = ">=3.10"
13
- keywords = ["plotagon", "tts", "video", "animation"]
14
- classifiers = [
15
- "Programming Language :: Python :: 3",
16
- "License :: OSI Approved :: MIT License",
17
- "Operating System :: OS Independent",
18
- ]
19
- dependencies = [
20
- "pyyaml",
21
- "edge-tts",
22
- "soundfile",
23
- "numpy",
24
- "truststore",
25
- ]
26
-
27
- [project.optional-dependencies]
28
- library = ["UnityPy"]
29
- storyteller = ["yt-dlp", "yapsnap"]
30
- mcp = ["mcp"]
31
- http = ["fastapi", "uvicorn"]
32
- dev = ["pytest", "UnityPy", "mcp", "fastapi", "uvicorn", "httpx", "jsonschema"]
33
-
34
- [project.urls]
35
- LinkedIn = "https://www.linkedin.com/in/benmallessa/"
36
-
37
- [project.scripts]
38
- plotagon-director = "plotagon_director.cli:main"
39
- plotagon-storyteller = "plotagon_director.storyteller:main"
40
-
41
- [tool.pytest.ini_options]
42
- testpaths = [
43
- "skills/plotagon-director/tests",
44
- "skills/plotagon-storyteller/tests",
45
- ]
46
-
47
- [tool.setuptools.packages.find]
48
- include = ["plotagon_director*"]
49
-
50
- [tool.setuptools.package-data]
51
- "plotagon_director.library" = ["data/*.json"]
52
- "plotagon_director" = ["schemas/*.json"]
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "plotagon-director"
7
+ version = "0.10.0"
8
+ description = "Génération de films Plotagon Studio Desktop (.plot) avec voix Edge TTS et synchro labiale"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Mohamed Amine Ben Mallessa" }]
12
+ requires-python = ">=3.10"
13
+ keywords = ["plotagon", "tts", "video", "animation"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ dependencies = [
20
+ "pyyaml",
21
+ "edge-tts",
22
+ "soundfile",
23
+ "numpy",
24
+ "truststore",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ library = ["UnityPy"]
29
+ storyteller = ["yt-dlp", "yapsnap"]
30
+ mcp = ["mcp"]
31
+ http = ["fastapi", "uvicorn"]
32
+ dev = ["pytest", "UnityPy", "mcp", "fastapi", "uvicorn", "httpx", "jsonschema"]
33
+
34
+ [project.urls]
35
+ LinkedIn = "https://www.linkedin.com/in/benmallessa/"
36
+
37
+ [project.scripts]
38
+ plotagon-director = "plotagon_director.cli:main"
39
+ plotagon-storyteller = "plotagon_director.storyteller:main"
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = [
43
+ "skills/plotagon-director/tests",
44
+ "skills/plotagon-storyteller/tests",
45
+ ]
46
+
47
+ [tool.setuptools.packages.find]
48
+ include = ["plotagon_director*"]
49
+
50
+ [tool.setuptools.package-data]
51
+ "plotagon_director.library" = ["data/*.json"]
52
+ "plotagon_director" = ["schemas/*.json"]
@@ -1,105 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://github.com/mohamed-amine-ben-mallessa/plotagon-director/blob/main/schemas/project.schema.json",
4
- "title": "Plotagon Director project",
5
- "description": "Scenario YAML pour plotagon-director. Les IDs (scene, character, expression, music, action, position) doivent exister dans la bibliotheque embarquee : les valider avec `plotagon-director validate --strict` ou explorer avec `plotagon-director search/list/positions`.",
6
- "type": "object",
7
- "required": ["project", "cast", "scenes"],
8
- "additionalProperties": false,
9
- "properties": {
10
- "project": {
11
- "type": "object",
12
- "required": ["title"],
13
- "additionalProperties": false,
14
- "properties": {
15
- "title": {"type": "string", "minLength": 1},
16
- "language": {
17
- "type": "string",
18
- "description": "Locale BCP 47, ex. fr-FR, en-US",
19
- "pattern": "^[a-z]{2}(-[A-Z]{2})?$",
20
- "default": "fr-FR"
21
- }
22
- }
23
- },
24
- "cast_file": {
25
- "type": "string",
26
- "description": "Chemin (relatif au project.yaml) d'un cast partage ; fusionne avant le cast local"
27
- },
28
- "cast": {
29
- "type": "array",
30
- "items": {
31
- "type": "object",
32
- "required": ["ref", "character"],
33
- "additionalProperties": false,
34
- "properties": {
35
- "ref": {"type": "string", "minLength": 1, "description": "Identifiant local reference par scenes[].actors et dialogue[].actor"},
36
- "character": {"type": "string", "pattern": "^[a-z0-9]+\\.[a-z0-9]+$", "description": "ID bibliotheque, ex. office.steve (53 disponibles : `plotagon-director list characters`)"},
37
- "name": {"type": "string", "description": "Nom affiche dans le film"},
38
- "voice": {"type": "string", "pattern": "^[a-z]{2}-[A-Z]{2}-[A-Za-z]+Neural$", "description": "Voix Edge TTS, ex. fr-FR-HenriNeural (docs/VOICES.md)"},
39
- "rate": {"type": "string", "pattern": "^[+-]\\d+%$", "description": "Debit TTS, ex. +10%"},
40
- "pitch": {"type": "string", "pattern": "^[+-]\\d+Hz$", "description": "Hauteur TTS, ex. -5Hz"}
41
- }
42
- }
43
- },
44
- "scenes": {
45
- "type": "array",
46
- "minItems": 1,
47
- "items": {
48
- "type": "object",
49
- "required": ["scene"],
50
- "additionalProperties": false,
51
- "properties": {
52
- "scene": {"type": "string", "pattern": "^[a-z0-9]+\\.[a-z0-9]+$", "description": "ID bibliotheque, ex. office.ceooffice (205 disponibles)"},
53
- "music": {"type": "string", "pattern": "^music\\.[a-z0-9]+$", "description": "38 pistes + music.stopmusic"},
54
- "actors": {
55
- "type": "array",
56
- "maxItems": 2,
57
- "items": {
58
- "type": "object",
59
- "required": ["ref"],
60
- "additionalProperties": false,
61
- "properties": {
62
- "ref": {"type": "string"},
63
- "position": {"type": "string", "description": "Slot propre a la scene (`plotagon-director positions <scene>`) ; omis = premier slot"}
64
- }
65
- }
66
- },
67
- "dialogue": {
68
- "type": "array",
69
- "items": {
70
- "oneOf": [
71
- {
72
- "type": "object",
73
- "required": ["actor", "text"],
74
- "additionalProperties": false,
75
- "properties": {
76
- "actor": {"type": "string"},
77
- "expression": {"type": "string", "default": "neutral", "description": "88 expressions (`plotagon-director list expressions`)"},
78
- "text": {"type": "string", "description": "Vide accepte (mime sans parole)"},
79
- "camera": {
80
- "type": "string",
81
- "enum": ["default", "establishing shot", "in dialogue mode", "in interaction mode", "moving right", "moving left", "moving forward", "moving backward", "wide shot", "still"],
82
- "description": "'in dialogue/interaction mode' exigent 2 acteurs ; 'moving *' exigent camera_position ET camera_target"
83
- },
84
- "camera_position": {"type": "string", "description": "ex. EstablishingLocation"},
85
- "camera_target": {"type": "string", "description": "Slot de position de la scene"}
86
- }
87
- },
88
- {
89
- "type": "object",
90
- "required": ["actor", "action", "target"],
91
- "additionalProperties": false,
92
- "properties": {
93
- "actor": {"type": "string"},
94
- "action": {"type": "string", "description": "15 interactions a 2 personnages (`plotagon-director list actions`)"},
95
- "target": {"type": "string", "description": "ref de l'autre acteur"}
96
- }
97
- }
98
- ]
99
- }
100
- }
101
- }
102
- }
103
- }
104
- }
105
- }